Skip to content

refactor: migrate cedar policy and group management to Angular signals#1688

Open
gugu wants to merge 10 commits intomainfrom
migrate-cedar-policy-groups-to-signals
Open

refactor: migrate cedar policy and group management to Angular signals#1688
gugu wants to merge 10 commits intomainfrom
migrate-cedar-policy-groups-to-signals

Conversation

@gugu
Copy link
Copy Markdown
Contributor

@gugu gugu commented Mar 25, 2026

Replace legacy Subject-based event bus in UsersService with httpResource for reactive data fetching and async ApiService methods for mutations. Migrate all dialog components, CedarPolicyList, CedarPolicyEditor, and UsersComponent to use signals, computed values, effects, and @if/@for control flow. Add name field to GroupUser interface.

Summary by CodeRabbit

  • New Features

    • Cedar policy engine integrated with editor validation and runtime permission checks; WASM runtime bundled for the frontend.
    • Group member names now displayed in the UI.
  • Refactor

    • Centralized permission checks and migrated many components to reactive signals and async flows; templates updated to new control-flow syntax.
  • Bug Fixes & Accessibility

    • Made interactive controls explicit buttons and improved ARIA attributes.
  • Chores

    • Added Cedar WASM dependency and included WASM asset in build.

Replace legacy Subject-based event bus in UsersService with httpResource
for reactive data fetching and async ApiService methods for mutations.
Migrate all dialog components, CedarPolicyList, CedarPolicyEditor, and
UsersComponent to use signals, computed values, effects, and @if/@for
control flow. Add name field to GroupUser interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Integrates Cedar WASM into the frontend (loader, validator, permission evaluator), migrates many components and services from accessLevel/Observables to Angular Signals, replaces template permission checks with Cedar-based canI/canEditConnection gates, updates build assets/deps for the WASM, and adds extensive WASM-backed tests.

Changes

Cohort / File(s) Summary
Build & Dependency
frontend/angular.json, frontend/package.json
Add cedar_wasm_bg.wasm to build assets and add runtime dependency @cedar-policy/cedar-wasm.
WASM loader / core services & tests
frontend/src/app/services/cedar-wasm.service.ts, frontend/src/app/services/cedar-permission.service.ts, frontend/src/app/services/cedar-validator.service.ts, frontend/src/app/services/cedar-permission.service.spec.ts
New Cedar WASM loader (fetch + init), permission evaluator (memoized signal-based canI + ready), validator (validate), and unit tests for permission/ready behavior.
Policy parsing / roundtrip tests
frontend/src/app/lib/cedar-policy-items.ts, frontend/src/app/lib/cedar-policy-parser.ts, frontend/src/app/lib/cedar-policy-roundtrip.spec.ts
Adjust policy item mappings (dashboard:create), update parser behavior, and add comprehensive WASM-backed round-trip tests.
Connections & visible tabs
frontend/src/app/services/connections.service.ts, .../connections.service.spec.ts
Inject CedarPermissionService/UsersService, add setActiveConnection on navigation, add canEditConnection(), and gate visible tabs using Cedar permissions.
Users service → Signals
frontend/src/app/services/users.service.ts, .../users.service.spec.ts
Migrate groups/groupUsers to Signals, add setActiveConnection/refreshGroups, async CRUD via ApiService, groupUsers caching, and groupsUpdated signaling; update tests to signal-based mocks.
UI permission refactor (components & templates)
many frontend/src/app/components/... (connect-db, connection-settings, dashboard, db-table-view, db-tables-list, db-table-row-edit, saved-filters-panel, etc.)
Remove accessLevel inputs, replace template checks with can* methods/signals or Cedar permission checks, add explicit type="button" and aria tweaks, and update bindings to call signals (e.g., foo()).
Dashboard / Table view changes
frontend/src/app/components/dashboard/*, frontend/src/app/components/dashboard/db-table-view/*, frontend/src/app/components/dashboard/db-tables-data-source.ts
Replace permissions inputs with Cedar-based computed checks/signals, compute actionsColumnWidth, set resource id for table permissions, and remove legacy permissions fields.
Cedar policy editor/list UI
frontend/src/app/components/users/cedar-policy-editor-dialog/*, frontend/src/app/components/users/cedar-policy-list/*
Migrate editor/list to Signals, add CedarValidatorService validation flows and UI for validationErrors, convert templates to block syntax, and update tests to expect signal reads/writes.
Dialogs & user-management → Signals + async
frontend/src/app/components/users/*-dialog/* (TS/HTML/spec/stories)
Replace constructor DI with inject(), convert submitting to signals, move async flows from subscribe to async/await, update templates/tests/stories to promise-based mocks and signal usage.
Templates: block syntax & accessibility
many frontend/src/app/components/**/*.html
Convert *ngIf/*ngFor to block syntax (@if/@for), switch property reads to signal calls, add aria attributes and type="button" to prevent implicit submits.
Models & small edits
frontend/src/app/models/user.ts
Add optional name?: string to GroupUser.
Widespread tests/spec updates
many frontend/src/app/**/*.spec.ts, story files
Update tests/mocks to inject new services, prefer Promise-based mocks over Observables where code changed, and assert signal values/behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Component / Template
    participant Perm as CedarPermissionService
    participant WASM as CedarWasmService
    participant Users as UsersService

    UI->>Perm: canI(action, type, resourceId)
    Perm->>Perm: check cache & prerequisites (user/groups, wasm)
    alt WASM not loaded
        Perm->>WASM: load()
        WASM-->>Perm: initialized cedar module
    end
    Perm->>Users: read current user's groups / merged policies
    Perm->>WASM: isAuthorized(principal, action, resource, staticPolicies)
    WASM-->>Perm: allow / deny / errors
    Perm-->>UI: update Signal<boolean|null>
    UI->>UI: template re-renders based on permission signal
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibbled at code through the night,

Loaded Cedar, made permissions light,
Signals hop where flags once stood,
WASM hums in the burrow—oh so good,
Hooray—new policies take flight!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main refactoring effort: migrating cedar policy and group management functionality to Angular signals.
Security Check ✅ Passed Cedar WASM integration properly validates policies before persistence with signal-based authorization checks and no hardcoded credentials.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate-cedar-policy-groups-to-signals

Comment @coderabbitai help to get the list of available commands and usage tips.

gugu and others added 3 commits March 25, 2026 14:33
Add @cedar-policy/cedar-wasm for client-side Cedar policy validation and
authorization. Extract shared WASM loading into CedarWasmService, add
input validation to the policy editor dialog, fix connection:edit
escalation bug, and introduce CedarPermissionService with signal-based
canI(action, resourceType, resourceId) for reactive permission checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate connection-level and group-level permission guards from
ConnectionsService properties (connectionAccessLevel, groupsAccessLevel,
isPermitted) to reactive CedarPermissionService.canI() signals across
Dashboard, ConnectionSettings, ConnectDB, and Users components. Load
groups globally on navigation so canI works app-wide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vice.canI

Migrate all table-level and connection-level permission checks to use
Cedar policy evaluation via canI(). Change canI return type from
Signal<boolean> to Signal<boolean | null> where null means "not yet
determined" (WASM/user/policies not loaded).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gugu gugu marked this pull request as ready for review March 26, 2026 09:17
Copilot AI review requested due to automatic review settings March 26, 2026 09:17
@coderabbitai coderabbitai bot requested a review from lyubov-voloshko March 26, 2026 09:19
…ion and deduplicating table permissions

Centralize canEditConnection in ConnectionsService to eliminate 6 duplicate canI() calls across components.
Extract _tablePermission helper in db-table-view and db-table-row-edit. Use @let in users template to cache
canManageGroup signal per group. Remove redundant _connectionsForPermissions aliases and unnecessary comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates user/group management and Cedar policy editing to Angular signals and introduces client-side Cedar (WASM) validation/authorization to drive UI permissions, replacing the legacy Subject-based event bus and access-level checks.

Changes:

  • Refactors UsersService to use httpResource for reactive group fetching, plus async ApiService mutations and signal-based update events.
  • Adds Cedar WASM loading + policy validation + canI() authorization services, and switches multiple UI areas (users/groups, dashboard/table actions, settings) to Cedar-driven permission checks.
  • Updates Users/Cedar policy components and related dialogs/tests to use signals and new @if/@for control flow; extends GroupUser with optional name.

Reviewed changes

Copilot reviewed 58 out of 59 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
frontend/yarn.lock Locks new dependency for Cedar WASM.
frontend/package.json Adds @cedar-policy/cedar-wasm.
frontend/angular.json Copies Cedar WASM binary into /assets/cedar-wasm.
frontend/src/app/models/user.ts Adds optional name to GroupUser.
frontend/src/app/services/users.service.ts Replaces Subject bus with signals + httpResource + async mutations.
frontend/src/app/services/users.service.spec.ts Updates tests for new signal/ApiService-based behavior.
frontend/src/app/services/connections.service.ts Sets active connection for UsersService; uses Cedar permissions for visible tabs.
frontend/src/app/services/connections.service.spec.ts Updates visibleTabs tests to use CedarPermissionService mock.
frontend/src/app/services/cedar-wasm.service.ts New service to lazy-load/init Cedar WASM.
frontend/src/app/services/cedar-validator.service.ts New service to validate Cedar policy text via WASM parse check.
frontend/src/app/services/cedar-permission.service.ts New client-side authorization service (canI) powered by Cedar WASM.
frontend/src/app/services/cedar-permission.service.spec.ts Adds coverage for canI, policy merge behavior, and readiness semantics.
frontend/src/app/lib/cedar-policy-roundtrip.spec.ts Adds round-trip tests for Cedar policy parse/serialize + wasm validation.
frontend/src/app/lib/cedar-policy-items.ts Adjusts permissions→policy-items mapping (no longer collapses connection edit into *).
frontend/src/app/components/users/users.component.ts Migrates users/groups UI state to signals and Cedar permissions.
frontend/src/app/components/users/users.component.spec.ts Updates tests to work with signal-based UsersService + permissions mock.
frontend/src/app/components/users/users.component.html Migrates to @if/@for and Cedar-driven UI permissions.
frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.ts Converts to async mutation + signal submitting.
frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.html Uses control flow + signal submitting(), adds explicit button types.
frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.spec.ts Updates tests for async flow + signal submitting.
frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.stories.ts Updates story mocks for async deleteGroupUser.
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.ts Converts to async addGroupUser + signal submitting.
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html Migrates to @if/@for and submitting() signal usage.
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts Updates tests for async flow + router provider changes.
frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.ts Converts to async rename + signal submitting.
frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.html Uses @if for validation error and submitting() in disabled state.
frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.spec.ts Updates to mock async rename method.
frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.ts Converts to async delete + signal submitting.
frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.html Uses submitting() and fixes button types/attrs.
frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.spec.ts Updates to mock async delete method.
frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.ts Converts to async create + signal submitting; uses ConnectionsService for connection id.
frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.html Uses @if for validation error and submitting() in disabled state.
frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts Updates to mock async create method.
frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.stories.ts Updates story mocks for async createGroup.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts Refactors to signal inputs/outputs and computed derived views.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.html Migrates to @if/@for and signal-based bindings.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts Updates inputs via setInput and output assertions.
frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts Converts editor state to signals; adds validation on mode switch + save.
frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.html Migrates to signal reads and renders validation errors in code mode.
frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.spec.ts Adds tests for validation-blocked saves and mode switches.
frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.css Ensures code editor fills container; styles validation errors.
frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts Replaces table permission checks with Cedar canI signals.
frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html Switches add/edit/delete UI gating to Cedar-based computed permissions.
frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts Replaces accessLevel input checks with Cedar connection edit permission.
frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html Gates folder/table reorder + actions via Cedar permission and adds explicit button types.
frontend/src/app/components/dashboard/db-tables-data-source.ts Removes AccessLevel gating for config alert; refactors minor code paths.
frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts Switches accessLevel input to Cedar-based connection edit permission.
frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html Gates saved-filter create/edit/delete UI via Cedar permission; adds explicit button types.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts Adds Cedar-based row/connection permissions and derives actions column width locally.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html Updates action buttons/menus to Cedar permissions; improves button typing and some ARIA.
frontend/src/app/components/dashboard/dashboard.component.ts Adds Cedar-based connection edit permission for error actions.
frontend/src/app/components/dashboard/dashboard.component.html Gates “Check database credentials” and AI-config alert visibility via Cedar permission.
frontend/src/app/components/connection-settings/connection-settings.component.ts Switches edit gating to Cedar connection edit permission.
frontend/src/app/components/connection-settings/connection-settings.component.html Uses Cedar permission for credential-check CTA and settings actions section.
frontend/src/app/components/connect-db/connect-db.component.ts Adds Cedar-based connection edit permission signal.
frontend/src/app/components/connect-db/connect-db.component.html Gates edit-connection actions via Cedar permission.
Comments suppressed due to low confidence (1)

frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts:163

  • actionsColumnWidth doesn’t account for tables without primary keys. TablesDataSource omits the actions column when keyAttributes.length === 0, but this getter may still return a non-zero width (based on permissions/actions), which can put the table into “with actions” layout even though the column isn’t rendered. Consider returning '0' when !this.tableData?.keyAttributes?.length (or when displayedColumns doesn’t include actions).
	public displayedColumns: string[] = [];
	public columnsToDisplay: string[] = [];
	public searchString: string;
	public staticSearchString: string;
	public bulkActions: CustomAction[];
	public bulkRows: string[];
	public displayedComparators = {
		eq: '=',
		gt: '>',

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to +105
async deleteGroup(groupId: string): Promise<void> {
await this._api.delete(`/group/${groupId}`, {
successMessage: 'Group has been removed.',
});
this._groupsUpdated.set('group-deleted');
}

async editGroupName(groupId: string, title: string): Promise<void> {
await this._api.put(
'/group/title',
{ title, groupId },
{
successMessage: 'Group name has been updated.',
},
);
this._groupsUpdated.set('group-renamed');
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mutation methods set groupsUpdated even when the underlying ApiService call fails (ApiService returns null on error instead of throwing). For example, deleteGroup/editGroupName/saveCedarPolicy/addGroupUser/deleteGroupUser will still emit an update event, triggering UI refreshes and potentially masking failures. Only set groupsUpdated when the request actually succeeds (e.g., check the return value from ApiService or change ApiService to throw on non-ok and set the signal in the success path).

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +70
async fetchGroupUsers(groupId: string): Promise<GroupUser[]> {
const users = await this._api.get<GroupUser[]>(`/group/users/${groupId}`);
const result = users ?? [];
this._groupUsers.update((current) => ({
...current,
[groupId]: result.length ? result : 'empty',
}));
return result;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchGroupUsers treats a null result from ApiService.get() (which happens on error/abort) as an empty user list and stores 'empty'. This makes the UI show “No users in the group” on request failures. Consider distinguishing “error” from “empty” (e.g., don’t update the cache when users === null, or store a separate error/loading state per group).

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +74
protected companyMembersWithoutAccess = computed(() => {
const members = this.companyMembers();
const users = this.groupUsers();
const allGroupUsers = Object.values(users).flat();
return differenceBy(members, allGroupUsers, 'email');
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

companyMembersWithoutAccess flattens groupUsers() values directly, but groupUsers stores the sentinel string 'empty' for empty groups. That means allGroupUsers can contain strings mixed with user objects, which can lead to incorrect comparisons and makes the code brittle. Filter to only include arrays (or change groupUsers to store empty arrays plus a separate “empty”/loading flag).

Copilot uses AI. Check for mistakes.
Comment on lines 123 to +126
label: tableWidgets[value].name || value,
value: tableRowValues[value],
required: tableRowRequiredValues[value],
readonly: (!permissions?.edit && pageAction !== 'dub') || pageMode === 'view',
readonly: (!canEditRow() && pageAction !== 'dub') || pageMode === 'view',
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field readonly state is now driven by canEditRow(). On the “Add new record” path (no primary keys in URL), the relevant permission is typically table:add, but canEditRow() may be false/null, which will make the form fields readonly even when the user is allowed to create rows. Consider basing readonly/submit enablement on canAddRow() for create mode (and canEditRow() for edit mode), and handling the null (loading) state explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +56
beforeAll(async () => {
const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
initSync({ module: new WebAssembly.Module(wasmBytes) });
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec fetches the WASM asset via fetch('/assets/...') in beforeAll. In typical unit test runners (Vitest/JSDOM/Node), relative-URL fetches to /assets often fail because no static asset server is running (or Node fetch requires an absolute URL). Consider loading the wasm bytes from the filesystem in tests, mocking fetch, or delegating initialization to CedarWasmService with a test double.

Copilot uses AI. Check for mistakes.
protected loading = signal(true);
protected formParseError = signal(false);
protected validationErrors = signal<string[]>([]);
protected validating = signal(false);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validating is declared but never read or written, which adds dead state and makes it unclear whether validation is intended to be surfaced in the UI. Either wire it into the validate paths (e.g., disable Save/toggles while validating) or remove it.

Suggested change
protected validating = signal(false);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +22
async load(): Promise<CedarWasmModule> {
if (this._module) return this._module;
if (this._loading) return this._loading;

this._loading = (async () => {
const cedar = await import('@cedar-policy/cedar-wasm/web');
const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
this._module = cedar;
return cedar;
})();

return this._loading;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load() caches the in-flight promise in _loading, but if the fetch/import/init fails the promise rejects and _loading is never cleared, so future calls will keep returning the same rejected promise and the app can’t recover without a reload. Wrap the async block in try/catch and reset _loading on failure; also consider checking response.ok before arrayBuffer() to surface a clearer error.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +50
private _signals = new Map<string, Signal<boolean | null>>();

constructor() {
this._cedarWasm.load().then((mod) => {
this._wasmModule.set(mod);
});
}

canI(action: string, resourceType: string, resourceId: string): Signal<boolean | null> {
const key = `${action}|${resourceType}|${resourceId}`;
let sig = this._signals.get(key);
if (!sig) {
sig = computed(() => this._evaluate(action, resourceType, resourceId));
this._signals.set(key, sig);
}
return sig;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canI() memoizes every (action, resourceType, resourceId) triple in _signals with no eviction. In screens that generate many unique resource IDs (e.g., lots of tables/rows/connections over time), this map can grow unbounded and retain computed signals indefinitely. Consider scoping the cache (e.g., per-connection), adding an eviction strategy, or exposing a way to clear it when the active connection/user changes.

Copilot uses AI. Check for mistakes.
<button mat-menu-item
<button type="button" mat-menu-item
role="menuitemcheckbox"
aria-checked="false"
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This button has both a static aria-checked="false" and a bound [attr.aria-checked]. The static value can be misleading and is redundant. Remove the hard-coded attribute and keep only the bound aria-checked (or use the appropriate ARIA role/attribute pattern for mat-menu-item with checkbox behavior).

Suggested change
aria-checked="false"

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +37
async joinGroupUser() {
this.submitting.set(true);
try {
await this._usersService.addGroupUser(this.data.group.id, this.groupUserEmail);
this._dialogRef.close();
this._angulartics2.eventTrack.next({
action: 'User groups: user was added to group successfully',
});
posthog.capture('User groups: user was added to group successfully');
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

joinGroupUser() always closes the dialog and emits “success” analytics after awaiting addGroupUser(). Since UsersService.addGroupUser() ultimately relies on ApiService (which returns null on failure instead of throwing), failures can still hit this success path. Consider making addGroupUser() throw/return a success boolean, and only close/track success when the request actually succeeds.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html (1)

205-209: ⚠️ Potential issue | 🟡 Minor

Add button missing permission check, and template should use @if instead of *ngIf.

The "Add" button (line 205) doesn't check canAddRow(), while the duplicate action and add link both do. Additionally, per coding guidelines, this template file should use built-in control flow syntax (@if) instead of structural directives (*ngIf).

Update line 205 to include the permission check and migrate to @if:

-                    <button *ngIf="!hasKeyAttributesFromURL" type="submit" mat-flat-button color="primary"
+                    <button `@if`="!hasKeyAttributesFromURL && canAddRow()" type="submit" mat-flat-button color="primary"
                         data-testid="record-add-button"
                         [disabled]="submitting || editRowForm.form.invalid">
                         Add
                     </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html`
around lines 205 - 209, The Add button currently uses a structural directive and
lacks a permission check; replace the "*ngIf" usage with the template
control-flow "@if" and include the permission guard by checking canAddRow()
alongside hasKeyAttributesFromURL (i.e., render the button only when
!hasKeyAttributesFromURL && canAddRow()). Keep the existing
attributes/data-testid, and preserve the disabled binding
([disabled]="submitting || editRowForm.form.invalid") and type/appearance so
behavior remains unchanged.
🧹 Nitpick comments (23)
frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html (1)

25-25: Consider adding track to @for when migrating control flow.

The *ngFor loop iterating over savedFilterData lacks a trackBy function. When this file is migrated to built-in control flow syntax, ensure you add a track expression (e.g., track filter.id) for better performance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html`
at line 25, The *ngFor on the mat-chip-option iterating savedFilterData should
use a track expression to maintain stable identities during migration to control
flow; update the loop over savedFilterData (the mat-chip-option element using
*ngFor) to include a track clause such as tracking a unique property (e.g.,
track filter.id) or a trackBy function that returns a unique id for each filter
to improve rendering performance and avoid unnecessary re-renders.
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html (1)

21-26: Consider using @else instead of a separate @if.

Since data.availableMembers.length and data.availableMembers.length === 0 are mutually exclusive, you can simplify this to an @else block:

♻️ Suggested refactor
     }
-    `@if` (data.availableMembers.length === 0) {
+    } `@else` {
       <p class="mat-body-2">
         All your company members are already in this group. To add a new one first add them to your company.
       </p>
-    }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html`
around lines 21 - 26, The second template conditional uses a separate "@if
(data.availableMembers.length === 0)" even though it is the exact inverse of the
preceding "@if (data.availableMembers.length > 0)"; change that second "@if" to
an "@else" so the mutually exclusive branches are simplified and easier to
maintain, locating the conditional that references data.availableMembers.length
in the user-add-dialog component template.
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts (1)

55-65: Consider extracting testable type alias for consistency with other dialog tests.

Other dialog tests (e.g., UserDeleteDialogComponent, GroupDeleteDialogComponent) define a separate *Testable type alias at the top of the file. This inline type cast works but is inconsistent with the established pattern.

♻️ Suggested refactor for consistency
+type UserAddTestable = UserAddDialogComponent & {
+	groupUserEmail: string;
+	joinGroupUser: () => Promise<void>;
+};
+
 describe('UserAddDialogComponent', () => {
   // ...
   it('should call add user service', async () => {
-    const testable = component as unknown as UserAddDialogComponent & {
-      groupUserEmail: string;
-      joinGroupUser: () => Promise<void>;
-    };
+    const testable = component as unknown as UserAddTestable;
     testable.groupUserEmail = 'user@test.com';

Based on learnings: "Use testable type aliases to access protected signals in tests instead of as any".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts`
around lines 55 - 65, The test uses an inline cast to access protected members
of UserAddDialogComponent; extract a reusable testable type alias (e.g.,
UserAddDialogComponentTestable) at the top of the spec to mirror other dialog
tests, then replace the inline cast with that alias when creating testable and
when accessing groupUserEmail and joinGroupUser so the spec is consistent with
UserDeleteDialogComponent and GroupDeleteDialogComponent patterns.
frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts (1)

123-128: Mixed dependency injection patterns: constructor injection and inject() in same component.

The component uses both constructor-based DI (lines 123-128) and inject() function (lines 80-81). Per coding guidelines, prefer the inject() function pattern consistently.

♻️ Suggested refactor for consistency
+	private _tableState = inject(TableStateService);
+	private _tablesService = inject(TablesService);
+	private _uiSettingsService = inject(UiSettingsService);
+	private dialog = inject(MatDialog);
+
-	constructor(
-		private _tableState: TableStateService,
-		private _tablesService: TablesService,
-		private _uiSettingsService: UiSettingsService,
-		private dialog: MatDialog,
-	) {}

Based on learnings: "Inject dependencies using the inject() function pattern".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts`
around lines 123 - 128, The component mixes constructor-based DI with the
inject() function (TableStateService, TablesService, UiSettingsService,
MatDialog vs existing inject() calls) — refactor to use a single pattern by
replacing the constructor parameters (private _tableState: TableStateService,
private _tablesService: TablesService, private _uiSettingsService:
UiSettingsService, private dialog: MatDialog) with inject() calls at the top of
the class (matching the existing inject() style used earlier) so all
dependencies are obtained via inject() and remove the empty constructor; ensure
property names and usages (e.g., _tableState, _tablesService,
_uiSettingsService, dialog) remain the same.
frontend/src/app/components/connect-db/connect-db.component.ts (1)

123-139: Mixed DI patterns: constructor injection and inject() function.

The component uses constructor-based DI for most services but inject() for CedarPermissionService. Per coding guidelines, prefer the inject() function pattern consistently throughout.

Based on learnings: "Inject dependencies using the inject() function pattern (e.g., private _http = inject(HttpClient))".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/connect-db/connect-db.component.ts` around lines
123 - 139, Switch the component to use the inject() pattern for dependencies
instead of constructor injection: replace the constructor-based injections for
ConnectionsService (_connections), NotificationsService (_notifications),
UserService (_user), CompanyService (_company), NgZone (ngZone), Router
(router), MatDialog (dialog), Angulartics2 (angulartics2), and Title (title)
with class fields initialized via inject(); remove the constructor and any
direct uses that rely on it, and move the canEditConnection initialization
(which currently calls this._permissions.canI(...)) into a lifecycle hook or a
class field initializer that runs after injection (e.g., set canEditConnection
in ngOnInit using the injected services); ensure CedarPermissionService usage
remains via inject() and update references to those symbols accordingly.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts (1)

36-48: Consider extracting repeated type annotation.

The type { action: string; tableName?: string; dashboardId?: string }[] is repeated across multiple tests. Extracting it to a type alias would improve maintainability.

♻️ Suggested refactor
+type PolicyPayload = { action: string; tableName?: string; dashboardId?: string };
+
 describe('CedarPolicyListComponent', () => {
   // ...
   
   it('should add a policy', () => {
-    let emitted: { action: string; tableName?: string; dashboardId?: string }[] | null = null;
+    let emitted: PolicyPayload[] | null = null;
     component.policiesChange.subscribe((v) => (emitted = v));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts`
around lines 36 - 48, The test repeats the inline type `{ action: string;
tableName?: string; dashboardId?: string }[]` for the emitted variable; extract
that to a local type alias (e.g., declare type PolicyList = { action: string;
tableName?: string; dashboardId?: string }[] ) at the top of the spec and
replace the inline annotations for `emitted` in the `it('should add a policy',
...)` test (and any other tests) so `emitted` is declared as `PolicyList |
null`, keeping the existing subscriptions to `component.policiesChange` and
assertions against `emitted` unchanged.
frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts (1)

62-71: Test doesn't verify dialog closes on success.

The test mocks mockDialogRef.close but doesn't assert it was called after successful group creation.

♻️ Add assertion for dialog close
 	it('should call create group service', async () => {
 		const testable = component as unknown as GroupAddTestable;
 		testable.connectionID = '12345678';
 		testable.groupTitle = 'Sellers';

 		await testable.addGroup();

 		expect(mockUsersService.createGroup).toHaveBeenCalledWith('12345678', 'Sellers');
 		expect(testable.submitting()).toBe(false);
+		expect(mockDialogRef.close).toHaveBeenCalled();
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts`
around lines 62 - 71, The test currently ensures mockUsersService.createGroup is
called and submitting() is false but does not assert the dialog is closed;
update the spec for GroupAddTestable.addGroup to also verify mockDialogRef.close
was invoked after a successful createGroup call (e.g., add an expectation that
mockDialogRef.close was called). Locate the test using symbols testable
(GroupAddTestable), addGroup(), mockUsersService.createGroup and
mockDialogRef.close and add the assertion immediately after the existing
expectations so the dialog-close behavior is validated.
frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts (1)

124-125: Inconsistent dependency injection pattern.

CedarPermissionService uses inject() while other dependencies use constructor injection. Consider migrating all dependencies to the inject() pattern for consistency.

This is a larger refactor that could be addressed in a follow-up PR to maintain consistency across the codebase. As per coding guidelines: "Inject dependencies using the inject() function pattern."

Also applies to: 140-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts`
around lines 124 - 125, The file mixes constructor injection with the standalone
inject() API (CedarPermissionService is assigned to private _permissions via
inject() while other services are injected in the constructor); migrate the
remaining constructor-injected dependencies in db-table-row-edit.component (the
constructor parameters) to use the inject() pattern and replace constructor
usage accordingly so all services use inject() (e.g., change
constructor-injected services to private fields assigned via inject(), remove
the constructor or keep it only for non-injectable logic, and update any
references to those services within methods to use the new private fields) to
make dependency injection consistent with the
CedarPermissionService/_permissions approach.
frontend/src/app/services/users.service.spec.ts (1)

149-253: New signal-based tests provide good coverage.

The tests properly verify:

  • Signal updates (groupsUpdated(), groupUsers())
  • ApiService method calls with correct parameters
  • Empty state handling ('empty' sentinel value)

Consider adding error scenario tests for the async methods to ensure proper error propagation.

♻️ Example error scenario test
it('should handle createGroup error', async () => {
  mockApi.post.mockRejectedValue(new Error('Network error'));

  await expect(service.createGroup('12345678', 'Managers')).rejects.toThrow('Network error');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.spec.ts` around lines 149 - 253, Add
negative-path unit tests that assert async methods propagate errors: for
createGroup, deleteGroup, editGroupName, saveCedarPolicy, addGroupUser,
deleteGroupUser, and fetchGroupUsers, mock the corresponding
mockApi.post/put/delete/get to mockRejectedValue(new Error('Network error')) and
assert await expect(service.<method>(...)).rejects.toThrow('Network error'); for
fetchGroupUsers also assert that when get rejects the service does not set
groupUsers()['<id>'] to a success value (or explicitly stays undefined), and use
the existing test helpers (mockApi, service) to mirror the success tests' call
args and signal checks.
frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts (2)

100-106: Mixed dependency injection patterns.

The component uses both the inject() function pattern (lines 63-64) and constructor-based DI (lines 100-106). For consistency with the coding guidelines, consider migrating all dependencies to use inject().

As per coding guidelines: "Inject dependencies using the inject() function pattern"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts`
around lines 100 - 106, The constructor currently injects MatDialog,
TablesService, ConnectionsService, Router and ActivatedRoute while the file also
uses inject() elsewhere; replace these constructor-based injections by calling
inject() for each dependency (e.g., const dialog = inject(MatDialog), const
_tables = inject(TablesService), const _connections =
inject(ConnectionsService), const router = inject(Router), const route =
inject(ActivatedRoute)) and remove the constructor entirely; ensure the property
names match existing usages in SavedFiltersPanelComponent so references to
dialog, _tables, _connections, router and route continue to work.

63-69: canEditConnection is computed once at instantiation, not reactively.

The canEditConnection signal is computed once when the component is instantiated, using currentConnectionID at that moment. If the connection changes while the component is alive (unlikely but possible), this permission check won't update.

If the component is recreated on route changes, this is likely fine. However, consider using a computed() signal if reactive updates are needed:

protected canEditConnection = computed(() => 
  this._permissions.canI('connection:edit', 'Connection', this._connectionsForPermissions.currentConnectionID)()
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts`
around lines 63 - 69, canEditConnection is evaluated once at instantiation and
won't update if the connection changes; wrap the permission check in a reactive
computed so it recalculates when currentConnectionID changes: replace the direct
assignment to protected canEditConnection with a computed(() => ...) that calls
this._permissions.canI('connection:edit', 'Connection',
this._connectionsForPermissions.currentConnectionID) and returns its current
boolean value (invoke the returned signal/function if canI returns one) so the
permission stays in sync with ConnectionsService.currentConnectionID.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html (1)

106-109: Redundant aria-checked attributes.

The button has both a static aria-checked="false" and a dynamic [attr.aria-checked]="column.selected". The static attribute is redundant and will be overridden by the dynamic binding.

🔧 Proposed fix
 <button type="button" mat-menu-item
     role="menuitemcheckbox"
-    aria-checked="false"
     [attr.aria-checked]="column.selected"
     *ngFor="let column of sortedColumns"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html`
around lines 106 - 109, Remove the redundant static aria-checked="false" from
the menu button that already uses the dynamic binding
[attr.aria-checked]="column.selected" (the button with role="menuitemcheckbox");
keep the dynamic binding so the accessible checked state comes from
column.selected and delete the hard-coded attribute to avoid
conflicting/duplicated ARIA values.
frontend/src/app/components/users/users.component.spec.ts (1)

47-50: Signal mock casts use as any instead of testable type aliases.

The signal mocks are cast with as any which violates the coding guideline about using testable type aliases to access protected signals in tests. Consider defining proper type aliases or adjusting the mock structure to avoid as any.

-	groups: mockGroups.asReadonly() as any,
-	groupsLoading: signal(false).asReadonly() as any,
-	groupUsers: signal({}).asReadonly() as any,
-	groupsUpdated: signal('').asReadonly() as any,
+	groups: mockGroups.asReadonly() as UsersService['groups'],
+	groupsLoading: signal(false).asReadonly() as UsersService['groupsLoading'],
+	groupUsers: signal({}).asReadonly() as UsersService['groupUsers'],
+	groupsUpdated: signal('').asReadonly() as UsersService['groupsUpdated'],

Based on learnings: "Use testable type aliases to access protected signals in tests instead of as any"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.spec.ts` around lines 47 -
50, Replace the unsafe "as any" casts for the mocked signals (groups,
groupsLoading, groupUsers, groupsUpdated) with explicit testable type aliases
that match the component's protected signal types; define a local alias (e.g.,
TestSignal<T> matching the Signal/WritableSignal shape used by the component)
and cast each mock to that alias instead of any so tests can read/inspect the
signal values while preserving type safety for groups, groupsLoading, groupUsers
and groupsUpdated.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts (1)

155-163: Consider converting actionsColumnWidth to a computed() signal.

The actionsColumnWidth getter recalculates on every access. Since it depends on signal values (canEditRow(), canAddRow(), canDeleteRow()), converting it to a computed() signal would provide memoization benefits.

♻️ Suggested refactor
-protected get actionsColumnWidth(): string {
-  const editCount = this.canEditRow() ? 1 : 0;
-  const addCount = this.canAddRow() ? 1 : 0;
-  const deleteCount = this.canDeleteRow() && this.tableData?.canDelete ? 1 : 0;
-  const defaultActionsCount = editCount + addCount + deleteCount;
-  const totalActionsCount = (this.tableData?.tableActions?.length || 0) + defaultActionsCount;
-  const lengthValue = totalActionsCount * 30 + 32;
-  return totalActionsCount === 0 ? '0' : `${lengthValue}px`;
-}
+protected actionsColumnWidth = computed(() => {
+  const editCount = this.canEditRow() ? 1 : 0;
+  const addCount = this.canAddRow() ? 1 : 0;
+  const deleteCount = this.canDeleteRow() && this.tableData?.canDelete ? 1 : 0;
+  const defaultActionsCount = editCount + addCount + deleteCount;
+  const totalActionsCount = (this.tableData?.tableActions?.length || 0) + defaultActionsCount;
+  const lengthValue = totalActionsCount * 30 + 32;
+  return totalActionsCount === 0 ? '0' : `${lengthValue}px`;
+});

Note: This would require updating template usages from actionsColumnWidth to actionsColumnWidth().

As per coding guidelines: "Use computed() for derived state in components and services"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts`
around lines 155 - 163, The getter actionsColumnWidth currently recomputes on
every access; replace it with a memoized computed signal by creating
actionsColumnWidth = computed(() => { ...same logic... }) that reads
canEditRow(), canAddRow(), canDeleteRow(), and this.tableData signals inside the
computed callback; update any template or internal usages from property access
to a function call (actionsColumnWidth()) and remove the existing protected get
actionsColumnWidth() getter so only the computed signal (imported from
`@angular/core`) remains.
frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.ts (1)

27-31: Rename addGroup() to match the edit flow.

This method calls editGroupName(), so addGroup() is misleading in the template and future call sites. saveGroupName() or renameGroup() would describe the current behavior.

As per coding guidelines, frontend/**/*.ts: Use camelCase for method names with verb-noun pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.ts`
around lines 27 - 31, The method name addGroup() is misleading because it calls
editGroupName(); rename addGroup() to a camelCase verb-noun name such as
saveGroupName() or renameGroup() (e.g., saveGroupName) and update all references
(component template bindings and any callers) to the new method name so the edit
flow is clear; ensure the method body (including
this._usersService.editGroupName and this.dialogRef.close) remains unchanged and
update any tests or event handlers that referenced addGroup().
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.ts (1)

26-32: Type the dialog payload instead of using any.

This flow depends on data.group.id, but any removes the compile-time check and the file still mixes constructor injection for MAT_DIALOG_DATA into otherwise inject()-based code. A small UserAddDialogData interface plus inject(MAT_DIALOG_DATA) would catch bad callers early.

As per coding guidelines, frontend/**/*.ts: Inject dependencies using the inject() function pattern (e.g., private _http = inject(HttpClient)) and **/*.{ts,tsx}: Avoid any types - use specific types or generics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.ts`
around lines 26 - 32, Replace the any-typed constructor injection with a typed
dialog payload and use Angular's inject() helper: define an interface
UserAddDialogData (e.g., { group: { id: string } } with the correct id type),
remove the constructor(`@Inject`(MAT_DIALOG_DATA) public data: any) pattern, and
instead create a class field like private data =
inject<UserAddDialogData>(MAT_DIALOG_DATA); update usages in joinGroupUser and
elsewhere (this.data.group.id, this.groupUserEmail, _usersService.addGroupUser)
to rely on that typed field and drop the biome-ignore comment so callers are
type-checked.
frontend/src/app/services/users.service.ts (1)

141-190: Legacy Observable methods retained for compatibility.

Keeping fetchConnectionUsers, fetchPermission, and updatePermission as Observable-based methods is pragmatic for incremental migration. Consider adding a comment or TODO indicating these could be migrated in a future PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 141 - 190, Add a
short comment/TODO above the legacy Observable-style methods
fetchConnectionUsers, fetchPermission, and updatePermission indicating they are
kept for compatibility with AuditComponent/permissions UI and could be migrated
to Promise/async-await (HTTPClient.toPromise()/firstValueFrom) in a future PR;
place the note near the top of the block (above fetchConnectionUsers) and
mention the specific callers (AuditComponent / permissions UI) so future
refactors know why they remain.
frontend/src/app/components/users/users.component.ts (1)

78-93: Effect-based event handling pattern is correct but may benefit from documentation.

The pattern of reading groupsUpdated(), processing based on the event type, then calling clearGroupsUpdated() within the same effect is valid. The clear at the end ensures the effect doesn't re-trigger for the same event. However, this pattern might be non-obvious to future maintainers.

Consider adding a brief comment explaining the event-processing-and-clear pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.ts` around lines 78 - 93,
Add a short explanatory comment above the effect describing the
read-process-clear event pattern: that effect() reads
this._usersService.groupsUpdated() once, branches on the returned action
('user-added'|'user-deleted' vs others), triggers either
fetchAllGroupUsers(currentGroups) or refreshGroups(), and then calls
clearGroupsUpdated() to prevent retriggering for the same event; reference the
involved symbols effect, this._usersService.groupsUpdated(),
this._usersService.clearGroupsUpdated(), fetchAllGroupUsers, refreshGroups, and
groups so future maintainers understand why the clear is inside the same effect.
frontend/src/app/lib/cedar-policy-roundtrip.spec.ts (1)

52-56: Consider adding error handling to WASM initialization in beforeAll.

If the WASM fetch fails (e.g., asset path misconfigured in test environment), all tests will fail with unclear errors. Adding explicit error handling would improve debuggability.

✨ Proposed improvement
 beforeAll(async () => {
-	const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
-	initSync({ module: new WebAssembly.Module(wasmBytes) });
+	const response = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm');
+	if (!response.ok) {
+		throw new Error(`Failed to load Cedar WASM: ${response.status} ${response.statusText}`);
+	}
+	const wasmBytes = await response.arrayBuffer();
+	initSync({ module: new WebAssembly.Module(wasmBytes) });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/lib/cedar-policy-roundtrip.spec.ts` around lines 52 - 56,
The WASM initialization in the beforeAll block (the fetch call for
'/assets/cedar-wasm/cedar_wasm_bg.wasm' and the initSync({ module: new
WebAssembly.Module(wasmBytes) })) lacks error handling; wrap the fetch +
WebAssembly.Module creation and initSync call in a try/catch, and on error throw
or fail the test with a clear message including the failing URL and the caught
error (or call console.error then rethrow) so test failures show a descriptive
initialization error instead of an opaque failure.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts (1)

204-206: Consider using computed() for editActionGroups.

editActionGroups is a getter that calls _buildFilteredGroups() on every access. If this is accessed multiple times in the template, it will recompute each time. Converting it to a computed() based on editingIndex would cache the result.

♻️ Proposed refactor to use computed()
+	private _editingIndex = signal<number | null>(null);
+
+	get editingIndex(): number | null {
+		return this._editingIndex();
+	}
+	set editingIndex(value: number | null) {
+		this._editingIndex.set(value);
+	}
+
+	protected editActionGroups = computed(() => this._buildFilteredGroups(this._editingIndex() ?? -1));

-	get editActionGroups(): PolicyActionGroup[] {
-		return this._buildFilteredGroups(this.editingIndex ?? -1);
-	}

Alternatively, if the template only accesses this once, the current implementation is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 204 - 206, The getter editActionGroups currently calls
_buildFilteredGroups(this.editingIndex ?? -1) on every access which can cause
repeated recomputation; replace the getter with a Vue computed property (import
computed from 'vue') that returns computed(() =>
this._buildFilteredGroups(this.editingIndex ?? -1)) so the result is cached and
only recalculated when editingIndex changes, and update references to use the
computed property consistently in the component/template.
frontend/src/app/services/cedar-permission.service.ts (2)

35-51: Potential unbounded cache growth in _signals Map.

The memoization Map grows without bound as new (action, resourceType, resourceId) combinations are requested. In long-running sessions with many dynamic resource IDs, this could lead to memory pressure.

Consider using a WeakRef-based cache, an LRU eviction strategy, or clearing the cache when the user changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/cedar-permission.service.ts` around lines 35 - 51,
The _signals Map used by canI(action, resourceType, resourceId) can grow
unbounded; modify the caching strategy to prevent memory leaks by replacing or
augmenting _signals with a bounded cache (e.g., an LRU with a max size) or use
WeakRef/FinalizationRegistry for entries tied to object keys, and ensure you
evict entries when the user context changes; update canI to insert into that
bounded cache and ensure computed signals created via computed(() =>
this._evaluate(...)) are removed/cleaned on eviction or user change, and add a
clearAll/clearForUser method (or integrate eviction in the
constructor/user-change handler) so _signals no longer grows indefinitely.

73-78: Silent permission denial on Cedar errors may obscure policy issues.

When isAuthorized returns an error, the code logs a warning but returns false, which denies access. This could make it difficult to diagnose malformed policies in production since users would simply see access denied without understanding why.

Consider exposing error state or distinguishing between "explicitly denied" and "evaluation failed" in the return type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/cedar-permission.service.ts` around lines 73 - 78,
The current isAuthorized implementation masks Cedar evaluation failures by
logging and returning false; update isAuthorized in cedar-permission.service.ts
to return a richer result (e.g., a union/DTO like { allowed: boolean; error?:
any; status: 'allowed'|'denied'|'failed' }) instead of a plain boolean so
callers can distinguish explicit denials from evaluation failures, and on
non-success (when result.type !== 'success') populate the error/details and
status='failed' (or alternatively throw a descriptive error) while keeping the
existing success path mapping decision === 'allow' to allowed=true and
status='allowed'.
frontend/src/app/components/dashboard/db-tables-data-source.ts (1)

340-349: Remove unused method parameters or document API consistency requirement.

The method changleColumnList receives _connectionId and _tableName parameters but does not use them. The underscore prefix indicates intentional disuse. Since the method is called from db-table-view.component.ts:727 with both parameters, either remove them and update the caller, or document if these parameters are required for API consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts` around lines
340 - 349, The method changleColumnList currently declares unused parameters
(_connectionId, _tableName); either remove those parameters from
changleColumnList and update its caller in db-table-view.component.ts (the call
at or near line where changleColumnList is invoked) to call it without
arguments, or if API consistency requires the parameters, keep them but remove
the underscore and add a short JSDoc on changleColumnList explaining they are
intentionally accepted for signature compatibility (and reference
connectionId/tableName) so they are not reported as unused; update all call
sites accordingly to match the chosen approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@frontend/src/app/components/connection-settings/connection-settings.component.ts`:
- Around line 122-123: Move the dependency injection and signal declaration to
the top of the component class: relocate the private _permissions =
inject(CedarPermissionService) and the protected canEditConnection:
ReturnType<CedarPermissionService['canI']> declarations so they appear
immediately after the other property declarations (before the constructor and
any methods); this ensures dependency injection and signal declarations are
grouped at the top of the class per the component structure guideline.

In `@frontend/src/app/components/dashboard/dashboard.component.ts`:
- Around line 117-121: The canEditConnection signal is being initialized with
the constructor-time value of this._connections.currentConnectionID, freezing
its key; change the initialization so canI is keyed to the live connection id
signal instead of a snapshot — call _permissions.canI with a reactive reference
(or a getter/closure) that reads this._connections.currentConnectionID so the
permission re-evaluates when the active connection changes (update the
initialization of canEditConnection to use this._connections.currentConnectionID
reactively rather than the current primitive value).
- Around line 184-224: getData() is re-subscribing to route.paramMap,
this._tableRow.cast and this._tables.cast each time it's called, leaking
handlers; fix by introducing a component-level destroy notifier (e.g., private
destroy$ = new Subject<void>()), pipe each subscription through
takeUntil(this.destroy$) where you subscribe to route.paramMap,
this._tableRow.cast and this._tables.cast inside getData(), and implement
ngOnDestroy to call this.destroy$.next() and this.destroy$.complete();
alternatively ensure those subscriptions are created once (outside repeated
getData() calls) and still use takeUntil to guarantee teardown.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts`:
- Around line 83-87: The constructor includes an unused parameter `_connections:
ConnectionsService`; remove this parameter from the constructor signature in
db-tables-data-source.ts so the class only injects the used services (`private
_tables: TablesService` and `private _tableRow: TableRowService`), and update
any related constructor parameter order if needed; ensure there are no remaining
references to `_connections` elsewhere in the class.

In
`@frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts`:
- Around line 168-185: Wrap calls to this._cedarValidator.validate(...) in
try/catch in both onEditorModeChange and savePolicy so validation rejections are
handled before any UI state changes; specifically, in onEditorModeChange (the
branch that switches to 'form') catch errors from await
this._cedarValidator.validate(policy) and set this.validationErrors or
this.formParseError appropriately and return instead of letting the rejection
bubble. In savePolicy, either move the validate call inside the existing try
block that controls submitting() or surround the validate call with its own
try/catch so a rejection does not occur before the try/finally that sets
submitting(true)/submitting(false); ensure you reference
_cedarValidator.validate, onEditorModeChange, savePolicy, submitting(),
validationErrors, and formParseError when making the change.
- Around line 64-65: availableDashboards is currently a signal that is set once
from this._dashboardsService.dashboards() inside a tables subscription, so it
doesn't update when dashboards or the active connection change; replace the
signal declaration with a computed that returns
this._dashboardsService.dashboards().map(d => ({ id: d.id, name: d.name })) so
availableDashboards reactively derives its value from the dashboards service
(leave availableTables as a signal if it's independent); update any usages to
read the computed value accordingly.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html`:
- Around line 30-35: The "Add" button is currently type="button" so it won't
trigger the form submission; update the button in the user-add-dialog template
(the button that references addUserForm.form.invalid and submitting()) to either
change type="submit" so it triggers the form's (ngSubmit) handler (e.g., the
component's submit method) or add an explicit (click) handler that calls the
component submit method (such as calling the existing submit/onSubmit function),
ensuring the submitting() guard still disables the button appropriately.

In `@frontend/src/app/components/users/users.component.html`:
- Around line 30-37: The edit button's click handler currently calls
openEditGroupNameDialog($event, groupItem.group) and posthog.capture(...) but
doesn't stop event propagation, causing the surrounding
mat-expansion-panel-header to toggle; update the click handler for the button
with class "title-edit-button" to call $event.stopPropagation() before invoking
openEditGroupNameDialog and posthog.capture (i.e., ensure stopPropagation is
executed first inside the click sequence for the button associated with
canManageGroup(groupItem.group.id) and the openEditGroupNameDialog method).

In `@frontend/src/app/components/users/users.component.ts`:
- Around line 116-122: The inner subscription to _company.fetchCompanyMembers
inside the subscription on _userService.cast can leak because it isn’t tied to
component teardown; replace the nested subscribe with a flattened operator
(e.g., use _userService.cast.pipe(takeUntilDestroyed(_destroyRef), tap(user =>
currentUser.set(user)), switchMap(user =>
_company.fetchCompanyMembers(user.company.id))) and then subscribe once to set
companyMembers) or alternatively apply takeUntilDestroyed to the inner
observable; ensure you import switchMap (and tap if used) from rxjs and update
the subscription to call companyMembers.set in the single downstream subscriber.
- Around line 125-129: canCreateGroup is being evaluated at field initialization
using _connections.currentConnectionID (which is null until NavigationEnd); move
its initialization into ngOnInit() or replace it with a getter so the permission
check uses the actual connectionID at runtime. Specifically, remove the
field-level assignment of canCreateGroup and instead set this.canCreateGroup =
this._permissions.canI('group:edit', 'Group',
this._connections.currentConnectionID) inside ngOnInit(), or implement a get
canCreateGroup() { return this._permissions.canI('group:edit','Group',
this._connections.currentConnectionID); } so the check uses the valid connection
ID (leave canManageGroup unchanged).

In `@frontend/src/app/services/cedar-permission.service.ts`:
- Around line 37-41: The constructor currently calls
this._cedarWasm.load().then(...) without handling rejections, so a failed WASM
load will leave _wasmModule null and cause canI() to return null forever; update
the constructor to add a .catch() (or use async/await with try/catch) on
this._cedarWasm.load() to log the error (e.g., via console.warn or a logger),
and optionally set a failure flag or a fallback value on _wasmModule so callers
of canI() can detect the failure instead of waiting indefinitely.

In `@frontend/src/app/services/cedar-wasm.service.ts`:
- Around line 14-23: The current load() async IIFE can leave this._loading as a
rejected promise if fetch or cedar.initSync fails; wrap the async initialization
inside a try/catch: perform the import/fetch/init in a local async
function/variable, and on success assign this._module and set this._loading to
the successful promise; on error, clear this._loading (set to undefined/null)
and this._module (if set) and rethrow the error so callers see the failure but
future load() calls can retry; reference the existing symbols _loading, load(),
this._module, cedar.initSync and the wasm fetch to locate where to add the
try/catch and reset logic.

In `@frontend/src/app/services/connections.service.ts`:
- Around line 126-133: visibleTabs and canEditConnection call
this._permissions.canI(...) with this.connectionID which may be null, causing
incorrect auth cache keys; add null guards so permission checks only run when
this.connectionID is truthy: in visibleTabs (method visibleTabs) wrap the
permission branches (permissions tab and connection-settings/edit-db tabs)
inside an if (this.connectionID) block and call canEditConnection() from there,
and update canEditConnection() to return false when this.connectionID is null
(e.g., return this.connectionID ?
this._permissions.canI('connection:edit','Connection',this.connectionID)() :
false).

In `@frontend/src/app/services/users.service.ts`:
- Around line 97-139: The four mutation methods editGroupName, saveCedarPolicy,
addGroupUser, and deleteGroupUser currently call this._groupsUpdated.set(...)
unconditionally; change each to capture the API call result (e.g., const res =
await this._api.put/post(...)) and only call this._groupsUpdated.set('...') when
the response is truthy/successful (same pattern as createGroup), so the event is
emitted only on successful operations.
- Around line 90-95: The methods deleteGroup, editGroupName, saveCedarPolicy,
addGroupUser, and deleteGroupUser currently call
this._groupsUpdated.set('group-...') (or similar) unconditionally; change each
to mirror createGroup by checking the API call's return value (the _api.* call
returns null on error) and only emit the update event when the call succeeded
(non-null/truthy response), i.e., perform the this._groupsUpdated.set(...)
inside an if that verifies the API result was successful.

---

Outside diff comments:
In
`@frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html`:
- Around line 205-209: The Add button currently uses a structural directive and
lacks a permission check; replace the "*ngIf" usage with the template
control-flow "@if" and include the permission guard by checking canAddRow()
alongside hasKeyAttributesFromURL (i.e., render the button only when
!hasKeyAttributesFromURL && canAddRow()). Keep the existing
attributes/data-testid, and preserve the disabled binding
([disabled]="submitting || editRowForm.form.invalid") and type/appearance so
behavior remains unchanged.

---

Nitpick comments:
In `@frontend/src/app/components/connect-db/connect-db.component.ts`:
- Around line 123-139: Switch the component to use the inject() pattern for
dependencies instead of constructor injection: replace the constructor-based
injections for ConnectionsService (_connections), NotificationsService
(_notifications), UserService (_user), CompanyService (_company), NgZone
(ngZone), Router (router), MatDialog (dialog), Angulartics2 (angulartics2), and
Title (title) with class fields initialized via inject(); remove the constructor
and any direct uses that rely on it, and move the canEditConnection
initialization (which currently calls this._permissions.canI(...)) into a
lifecycle hook or a class field initializer that runs after injection (e.g., set
canEditConnection in ngOnInit using the injected services); ensure
CedarPermissionService usage remains via inject() and update references to those
symbols accordingly.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html`:
- Around line 106-109: Remove the redundant static aria-checked="false" from the
menu button that already uses the dynamic binding
[attr.aria-checked]="column.selected" (the button with role="menuitemcheckbox");
keep the dynamic binding so the accessible checked state comes from
column.selected and delete the hard-coded attribute to avoid
conflicting/duplicated ARIA values.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts`:
- Around line 155-163: The getter actionsColumnWidth currently recomputes on
every access; replace it with a memoized computed signal by creating
actionsColumnWidth = computed(() => { ...same logic... }) that reads
canEditRow(), canAddRow(), canDeleteRow(), and this.tableData signals inside the
computed callback; update any template or internal usages from property access
to a function call (actionsColumnWidth()) and remove the existing protected get
actionsColumnWidth() getter so only the computed signal (imported from
`@angular/core`) remains.

In
`@frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html`:
- Line 25: The *ngFor on the mat-chip-option iterating savedFilterData should
use a track expression to maintain stable identities during migration to control
flow; update the loop over savedFilterData (the mat-chip-option element using
*ngFor) to include a track clause such as tracking a unique property (e.g.,
track filter.id) or a trackBy function that returns a unique id for each filter
to improve rendering performance and avoid unnecessary re-renders.

In
`@frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts`:
- Around line 100-106: The constructor currently injects MatDialog,
TablesService, ConnectionsService, Router and ActivatedRoute while the file also
uses inject() elsewhere; replace these constructor-based injections by calling
inject() for each dependency (e.g., const dialog = inject(MatDialog), const
_tables = inject(TablesService), const _connections =
inject(ConnectionsService), const router = inject(Router), const route =
inject(ActivatedRoute)) and remove the constructor entirely; ensure the property
names match existing usages in SavedFiltersPanelComponent so references to
dialog, _tables, _connections, router and route continue to work.
- Around line 63-69: canEditConnection is evaluated once at instantiation and
won't update if the connection changes; wrap the permission check in a reactive
computed so it recalculates when currentConnectionID changes: replace the direct
assignment to protected canEditConnection with a computed(() => ...) that calls
this._permissions.canI('connection:edit', 'Connection',
this._connectionsForPermissions.currentConnectionID) and returns its current
boolean value (invoke the returned signal/function if canI returns one) so the
permission stays in sync with ConnectionsService.currentConnectionID.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts`:
- Around line 340-349: The method changleColumnList currently declares unused
parameters (_connectionId, _tableName); either remove those parameters from
changleColumnList and update its caller in db-table-view.component.ts (the call
at or near line where changleColumnList is invoked) to call it without
arguments, or if API consistency requires the parameters, keep them but remove
the underscore and add a short JSDoc on changleColumnList explaining they are
intentionally accepted for signature compatibility (and reference
connectionId/tableName) so they are not reported as unused; update all call
sites accordingly to match the chosen approach.

In
`@frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts`:
- Around line 123-128: The component mixes constructor-based DI with the
inject() function (TableStateService, TablesService, UiSettingsService,
MatDialog vs existing inject() calls) — refactor to use a single pattern by
replacing the constructor parameters (private _tableState: TableStateService,
private _tablesService: TablesService, private _uiSettingsService:
UiSettingsService, private dialog: MatDialog) with inject() calls at the top of
the class (matching the existing inject() style used earlier) so all
dependencies are obtained via inject() and remove the empty constructor; ensure
property names and usages (e.g., _tableState, _tablesService,
_uiSettingsService, dialog) remain the same.

In
`@frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts`:
- Around line 124-125: The file mixes constructor injection with the standalone
inject() API (CedarPermissionService is assigned to private _permissions via
inject() while other services are injected in the constructor); migrate the
remaining constructor-injected dependencies in db-table-row-edit.component (the
constructor parameters) to use the inject() pattern and replace constructor
usage accordingly so all services use inject() (e.g., change
constructor-injected services to private fields assigned via inject(), remove
the constructor or keep it only for non-injectable logic, and update any
references to those services within methods to use the new private fields) to
make dependency injection consistent with the
CedarPermissionService/_permissions approach.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts`:
- Around line 36-48: The test repeats the inline type `{ action: string;
tableName?: string; dashboardId?: string }[]` for the emitted variable; extract
that to a local type alias (e.g., declare type PolicyList = { action: string;
tableName?: string; dashboardId?: string }[] ) at the top of the spec and
replace the inline annotations for `emitted` in the `it('should add a policy',
...)` test (and any other tests) so `emitted` is declared as `PolicyList |
null`, keeping the existing subscriptions to `component.policiesChange` and
assertions against `emitted` unchanged.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 204-206: The getter editActionGroups currently calls
_buildFilteredGroups(this.editingIndex ?? -1) on every access which can cause
repeated recomputation; replace the getter with a Vue computed property (import
computed from 'vue') that returns computed(() =>
this._buildFilteredGroups(this.editingIndex ?? -1)) so the result is cached and
only recalculated when editingIndex changes, and update references to use the
computed property consistently in the component/template.

In
`@frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts`:
- Around line 62-71: The test currently ensures mockUsersService.createGroup is
called and submitting() is false but does not assert the dialog is closed;
update the spec for GroupAddTestable.addGroup to also verify mockDialogRef.close
was invoked after a successful createGroup call (e.g., add an expectation that
mockDialogRef.close was called). Locate the test using symbols testable
(GroupAddTestable), addGroup(), mockUsersService.createGroup and
mockDialogRef.close and add the assertion immediately after the existing
expectations so the dialog-close behavior is validated.

In
`@frontend/src/app/components/users/group-name-edit-dialog/group-name-edit-dialog.component.ts`:
- Around line 27-31: The method name addGroup() is misleading because it calls
editGroupName(); rename addGroup() to a camelCase verb-noun name such as
saveGroupName() or renameGroup() (e.g., saveGroupName) and update all references
(component template bindings and any callers) to the new method name so the edit
flow is clear; ensure the method body (including
this._usersService.editGroupName and this.dialogRef.close) remains unchanged and
update any tests or event handlers that referenced addGroup().

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html`:
- Around line 21-26: The second template conditional uses a separate "@if
(data.availableMembers.length === 0)" even though it is the exact inverse of the
preceding "@if (data.availableMembers.length > 0)"; change that second "@if" to
an "@else" so the mutually exclusive branches are simplified and easier to
maintain, locating the conditional that references data.availableMembers.length
in the user-add-dialog component template.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts`:
- Around line 55-65: The test uses an inline cast to access protected members of
UserAddDialogComponent; extract a reusable testable type alias (e.g.,
UserAddDialogComponentTestable) at the top of the spec to mirror other dialog
tests, then replace the inline cast with that alias when creating testable and
when accessing groupUserEmail and joinGroupUser so the spec is consistent with
UserDeleteDialogComponent and GroupDeleteDialogComponent patterns.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.ts`:
- Around line 26-32: Replace the any-typed constructor injection with a typed
dialog payload and use Angular's inject() helper: define an interface
UserAddDialogData (e.g., { group: { id: string } } with the correct id type),
remove the constructor(`@Inject`(MAT_DIALOG_DATA) public data: any) pattern, and
instead create a class field like private data =
inject<UserAddDialogData>(MAT_DIALOG_DATA); update usages in joinGroupUser and
elsewhere (this.data.group.id, this.groupUserEmail, _usersService.addGroupUser)
to rely on that typed field and drop the biome-ignore comment so callers are
type-checked.

In `@frontend/src/app/components/users/users.component.spec.ts`:
- Around line 47-50: Replace the unsafe "as any" casts for the mocked signals
(groups, groupsLoading, groupUsers, groupsUpdated) with explicit testable type
aliases that match the component's protected signal types; define a local alias
(e.g., TestSignal<T> matching the Signal/WritableSignal shape used by the
component) and cast each mock to that alias instead of any so tests can
read/inspect the signal values while preserving type safety for groups,
groupsLoading, groupUsers and groupsUpdated.

In `@frontend/src/app/components/users/users.component.ts`:
- Around line 78-93: Add a short explanatory comment above the effect describing
the read-process-clear event pattern: that effect() reads
this._usersService.groupsUpdated() once, branches on the returned action
('user-added'|'user-deleted' vs others), triggers either
fetchAllGroupUsers(currentGroups) or refreshGroups(), and then calls
clearGroupsUpdated() to prevent retriggering for the same event; reference the
involved symbols effect, this._usersService.groupsUpdated(),
this._usersService.clearGroupsUpdated(), fetchAllGroupUsers, refreshGroups, and
groups so future maintainers understand why the clear is inside the same effect.

In `@frontend/src/app/lib/cedar-policy-roundtrip.spec.ts`:
- Around line 52-56: The WASM initialization in the beforeAll block (the fetch
call for '/assets/cedar-wasm/cedar_wasm_bg.wasm' and the initSync({ module: new
WebAssembly.Module(wasmBytes) })) lacks error handling; wrap the fetch +
WebAssembly.Module creation and initSync call in a try/catch, and on error throw
or fail the test with a clear message including the failing URL and the caught
error (or call console.error then rethrow) so test failures show a descriptive
initialization error instead of an opaque failure.

In `@frontend/src/app/services/cedar-permission.service.ts`:
- Around line 35-51: The _signals Map used by canI(action, resourceType,
resourceId) can grow unbounded; modify the caching strategy to prevent memory
leaks by replacing or augmenting _signals with a bounded cache (e.g., an LRU
with a max size) or use WeakRef/FinalizationRegistry for entries tied to object
keys, and ensure you evict entries when the user context changes; update canI to
insert into that bounded cache and ensure computed signals created via
computed(() => this._evaluate(...)) are removed/cleaned on eviction or user
change, and add a clearAll/clearForUser method (or integrate eviction in the
constructor/user-change handler) so _signals no longer grows indefinitely.
- Around line 73-78: The current isAuthorized implementation masks Cedar
evaluation failures by logging and returning false; update isAuthorized in
cedar-permission.service.ts to return a richer result (e.g., a union/DTO like {
allowed: boolean; error?: any; status: 'allowed'|'denied'|'failed' }) instead of
a plain boolean so callers can distinguish explicit denials from evaluation
failures, and on non-success (when result.type !== 'success') populate the
error/details and status='failed' (or alternatively throw a descriptive error)
while keeping the existing success path mapping decision === 'allow' to
allowed=true and status='allowed'.

In `@frontend/src/app/services/users.service.spec.ts`:
- Around line 149-253: Add negative-path unit tests that assert async methods
propagate errors: for createGroup, deleteGroup, editGroupName, saveCedarPolicy,
addGroupUser, deleteGroupUser, and fetchGroupUsers, mock the corresponding
mockApi.post/put/delete/get to mockRejectedValue(new Error('Network error')) and
assert await expect(service.<method>(...)).rejects.toThrow('Network error'); for
fetchGroupUsers also assert that when get rejects the service does not set
groupUsers()['<id>'] to a success value (or explicitly stays undefined), and use
the existing test helpers (mockApi, service) to mirror the success tests' call
args and signal checks.

In `@frontend/src/app/services/users.service.ts`:
- Around line 141-190: Add a short comment/TODO above the legacy
Observable-style methods fetchConnectionUsers, fetchPermission, and
updatePermission indicating they are kept for compatibility with
AuditComponent/permissions UI and could be migrated to Promise/async-await
(HTTPClient.toPromise()/firstValueFrom) in a future PR; place the note near the
top of the block (above fetchConnectionUsers) and mention the specific callers
(AuditComponent / permissions UI) so future refactors know why they remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +122 to +123
private _permissions = inject(CedarPermissionService);
protected canEditConnection: ReturnType<CedarPermissionService['canI']>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Dependency injection and signal declarations are misplaced in class structure.

The _permissions injection and canEditConnection declaration are placed in the middle of the class (after the constructor, before methods), violating the component structure guideline. Move these to the top of the class, right after the existing property declarations.

♻️ Suggested reorganization
 export class ConnectionSettingsComponent implements OnInit {
   protected posthog = posthog;
   public isSaas = (environment as any).saas;
+  private _permissions = inject(CedarPermissionService);
+  protected canEditConnection: ReturnType<CedarPermissionService['canI']>;
   public connectionID: string | null = null;
   // ... other properties ...

-  private _permissions = inject(CedarPermissionService);
-  protected canEditConnection: ReturnType<CedarPermissionService['canI']>;

   getSettings() {
     // ...
   }

As per coding guidelines: "Structure components with dependency injection at the top, followed by signals, computed signals, effects, lifecycle hooks, public methods, then private methods"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/connection-settings/connection-settings.component.ts`
around lines 122 - 123, Move the dependency injection and signal declaration to
the top of the component class: relocate the private _permissions =
inject(CedarPermissionService) and the protected canEditConnection:
ReturnType<CedarPermissionService['canI']> declarations so they appear
immediately after the other property declarations (before the constructor and
any methods); this ensures dependency injection and signal declarations are
grouped at the top of the class per the component structure guideline.

Comment on lines +184 to +224
this.route.paramMap
.pipe(
map((params: ParamMap) => {
let tableName = params.get('table-name');
if (tableName) {
this.selectedTableName = tableName;
this.setTable(tableName);
console.log('setTable from getData paramMap');
this.title.setTitle(
`${this.selectedTableDisplayName} table | ${this._company.companyTabTitle || 'Rocketadmin'}`,
);
this.selection.clear();
} else {
tableName = this.allTables[0]?.table;
if (this.defaultTableToOpen) {
tableName = this.defaultTableToOpen;
} else {
tableName = this.allTables[0]?.table;
}
this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true });
this.selectedTableName = tableName;
}
this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true });
this.selectedTableName = tableName;
}
}),
)
.subscribe();
this._tableRow.cast.subscribe((arg) => {
if (arg === 'delete row' && this.selectedTableName) {
this.setTable(this.selectedTableName);
console.log('setTable from getData _tableRow cast');
this.selection.clear();
}
});
this._tables.cast.subscribe((arg) => {
if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) {
this.setTable(this.selectedTableName);
console.log('setTable from getData _tables cast');
this.selection.clear();
}
if (arg === 'activate actions') {
this.selection.clear();
}
});
}
},
(err) => {
this.isServerError = true;
this.serverError = { abstract: err.error?.message || err.message, details: err.error?.originalMessage };
this.loading = false;
this.title.setTitle(`Error | ${this._company.companyTabTitle || 'Rocketadmin'}`);
});
}),
)
.subscribe();
this._tableRow.cast.subscribe((arg) => {
if (arg === 'delete row' && this.selectedTableName) {
this.setTable(this.selectedTableName);
console.log('setTable from getData _tableRow cast');
this.selection.clear();
}
});
this._tables.cast.subscribe((arg) => {
if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) {
this.setTable(this.selectedTableName);
console.log('setTable from getData _tables cast');
this.selection.clear();
}
if (arg === 'activate actions') {
this.selection.clear();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

getData() is accumulating subscriptions on every refresh.

Each successful fetch adds fresh subscriptions to route.paramMap, TableRowService.cast, and TablesService.cast. Because getData() is called again from the UI settings stream, those handlers start firing multiple times and none of them are torn down on destroy.

Based on learnings, frontend/**/*.component.ts: Use takeUntil pattern for memory leak prevention with proper subscription management.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/dashboard/dashboard.component.ts` around lines
184 - 224, getData() is re-subscribing to route.paramMap, this._tableRow.cast
and this._tables.cast each time it's called, leaking handlers; fix by
introducing a component-level destroy notifier (e.g., private destroy$ = new
Subject<void>()), pipe each subscription through takeUntil(this.destroy$) where
you subscribe to route.paramMap, this._tableRow.cast and this._tables.cast
inside getData(), and implement ngOnDestroy to call this.destroy$.next() and
this.destroy$.complete(); alternatively ensure those subscriptions are created
once (outside repeated getData() calls) and still use takeUntil to guarantee
teardown.

Comment on lines 83 to 87
constructor(
private _tables: TablesService,
private _connections: ConnectionsService,
_connections: ConnectionsService,
private _tableRow: TableRowService,
) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any usage of _connections in the file
rg -n '_connections' frontend/src/app/components/dashboard/db-tables-data-source.ts

Repository: rocket-admin/rocketadmin

Length of output: 106


Remove unused constructor parameter _connections.

The _connections parameter at line 85 is never used in this class. It should be removed to keep the constructor clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts` around lines
83 - 87, The constructor includes an unused parameter `_connections:
ConnectionsService`; remove this parameter from the constructor signature in
db-tables-data-source.ts so the class only injects the used services (`private
_tables: TablesService` and `private _tableRow: TableRowService`), and update
any related constructor parameter order if needed; ensure there are no remaining
references to `_connections` elsewhere in the class.

Comment on lines +64 to +65
protected availableTables = signal<AvailableTable[]>([]);
protected availableDashboards = signal<AvailableDashboard[]>([]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "cedar-policy-editor-dialog.component.ts" | head -5

Repository: rocket-admin/rocketadmin

Length of output: 170


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '50,75p'

Repository: rocket-admin/rocketadmin

Length of output: 1384


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '130,150p'

Repository: rocket-admin/rocketadmin

Length of output: 809


🏁 Script executed:

wc -l frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts

Repository: rocket-admin/rocketadmin

Length of output: 172


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '100,160p'

Repository: rocket-admin/rocketadmin

Length of output: 2200


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '75,130p'

Repository: rocket-admin/rocketadmin

Length of output: 2189


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '160,230p'

Repository: rocket-admin/rocketadmin

Length of output: 2646


🏁 Script executed:

grep -n "dashboards\|validatePolicy\|savePolicy" frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts

Repository: rocket-admin/rocketadmin

Length of output: 359


🏁 Script executed:

grep -A 5 -B 5 "computed" frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | head -20

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

grep -n "class CedarPolicyEditorDialogComponent" frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts

Repository: rocket-admin/rocketadmin

Length of output: 136


🏁 Script executed:

grep -n "import.*computed\|from '@angular" frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | head -20

Repository: rocket-admin/rocketadmin

Length of output: 582


🏁 Script executed:

cat -n frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts | sed -n '1,45p'

Repository: rocket-admin/rocketadmin

Length of output: 2528


🏁 Script executed:

cat -n frontend/src/app/services/dashboards.service.ts | grep -A 3 "dashboards()"

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

grep -n "dashboards\|signal\|computed" frontend/src/app/services/dashboards.service.ts | head -15

Repository: rocket-admin/rocketadmin

Length of output: 1139


🏁 Script executed:

grep -n "availableDashboards" frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts

Repository: rocket-admin/rocketadmin

Length of output: 175


🏁 Script executed:

grep -n "availableDashboards" frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts | head -10

Repository: rocket-admin/rocketadmin

Length of output: 231


🏁 Script executed:

cat -n frontend/src/app/services/dashboards.service.ts | sed -n '1,60p'

Repository: rocket-admin/rocketadmin

Length of output: 2829


Use computed() for derived state: availableDashboards should reactively track dashboards.

Currently, availableDashboards snapshots this._dashboardsService.dashboards() once inside the tables subscription callback (lines 138–143). If dashboards load, change, or the active connection updates after tables are fetched, the form never sees the updated list. Use computed() to create a reactive derivation:

protected availableDashboards = computed(() =>
  this._dashboardsService.dashboards().map((d) => ({
    id: d.id,
    name: d.name,
  }))
);

This ensures the form always displays current dashboards as per the coding guideline: "Use computed() for derived state in components and services."

Also applies to: Line 64.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/cedar-policy-editor-dialog/cedar-policy-editor-dialog.component.ts`
around lines 64 - 65, availableDashboards is currently a signal that is set once
from this._dashboardsService.dashboards() inside a tables subscription, so it
doesn't update when dashboards or the active connection change; replace the
signal declaration with a computed that returns
this._dashboardsService.dashboards().map(d => ({ id: d.id, name: d.name })) so
availableDashboards reactively derives its value from the dashboards service
(leave availableTables as a signal if it's independent); update any usages to
read the computed value accordingly.

Comment on lines +37 to +41
constructor() {
this._cedarWasm.load().then((mod) => {
this._wasmModule.set(mod);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for WASM loading.

If this._cedarWasm.load() rejects, the error is silently swallowed and _wasmModule remains null, causing all canI() calls to return null indefinitely. Consider handling the error to at least log a warning.

🛡️ Proposed fix to add error handling
 constructor() {
-	this._cedarWasm.load().then((mod) => {
-		this._wasmModule.set(mod);
-	});
+	this._cedarWasm.load()
+		.then((mod) => {
+			this._wasmModule.set(mod);
+		})
+		.catch((err) => {
+			console.error('Failed to load Cedar WASM module:', err);
+		});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor() {
this._cedarWasm.load().then((mod) => {
this._wasmModule.set(mod);
});
}
constructor() {
this._cedarWasm.load()
.then((mod) => {
this._wasmModule.set(mod);
})
.catch((err) => {
console.error('Failed to load Cedar WASM module:', err);
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/cedar-permission.service.ts` around lines 37 - 41,
The constructor currently calls this._cedarWasm.load().then(...) without
handling rejections, so a failed WASM load will leave _wasmModule null and cause
canI() to return null forever; update the constructor to add a .catch() (or use
async/await with try/catch) on this._cedarWasm.load() to log the error (e.g.,
via console.warn or a logger), and optionally set a failure flag or a fallback
value on _wasmModule so callers of canI() can detect the failure instead of
waiting indefinitely.

Comment on lines +14 to +23
this._loading = (async () => {
const cedar = await import('@cedar-policy/cedar-wasm/web');
const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
this._module = cedar;
return cedar;
})();

return this._loading;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling leaves service in broken state after failure.

If the WASM fetch or initialization fails, _loading remains set to the rejected promise. Subsequent calls to load() will return this rejected promise forever, preventing recovery.

🐛 Proposed fix to handle errors
 	async load(): Promise<CedarWasmModule> {
 		if (this._module) return this._module;
 		if (this._loading) return this._loading;

 		this._loading = (async () => {
-			const cedar = await import('@cedar-policy/cedar-wasm/web');
-			const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
-			cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
-			this._module = cedar;
-			return cedar;
+			try {
+				const cedar = await import('@cedar-policy/cedar-wasm/web');
+				const response = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm');
+				if (!response.ok) {
+					throw new Error(`Failed to fetch WASM: ${response.status}`);
+				}
+				const wasmBytes = await response.arrayBuffer();
+				cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
+				this._module = cedar;
+				return cedar;
+			} catch (error) {
+				this._loading = null; // Allow retry on next call
+				throw error;
+			}
 		})();

 		return this._loading;
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._loading = (async () => {
const cedar = await import('@cedar-policy/cedar-wasm/web');
const wasmBytes = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm').then((r) => r.arrayBuffer());
cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
this._module = cedar;
return cedar;
})();
return this._loading;
}
async load(): Promise<CedarWasmModule> {
if (this._module) return this._module;
if (this._loading) return this._loading;
this._loading = (async () => {
try {
const cedar = await import('@cedar-policy/cedar-wasm/web');
const response = await fetch('/assets/cedar-wasm/cedar_wasm_bg.wasm');
if (!response.ok) {
throw new Error(`Failed to fetch WASM: ${response.status}`);
}
const wasmBytes = await response.arrayBuffer();
cedar.initSync({ module: new WebAssembly.Module(wasmBytes) });
this._module = cedar;
return cedar;
} catch (error) {
this._loading = null; // Allow retry on next call
throw error;
}
})();
return this._loading;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/cedar-wasm.service.ts` around lines 14 - 23, The
current load() async IIFE can leave this._loading as a rejected promise if fetch
or cedar.initSync fails; wrap the async initialization inside a try/catch:
perform the import/fetch/init in a local async function/variable, and on success
assign this._module and set this._loading to the successful promise; on error,
clear this._loading (set to undefined/null) and this._module (if set) and
rethrow the error so callers see the failure but future load() calls can retry;
reference the existing symbols _loading, load(), this._module, cedar.initSync
and the wasm fetch to locate where to add the try/catch and reset logic.

Comment on lines 126 to 133
get visibleTabs() {
let tabs = ['dashboard', 'dashboards', 'audit'];
if (this.groupsAccessLevel) tabs.push('permissions');
if (this.isPermitted(this.connectionAccessLevel)) tabs.push('connection-settings', 'edit-db');
const tabs = ['dashboard', 'dashboards', 'audit'];
if (this._permissions.canI('group:read', 'Group', this.connectionID)()) tabs.push('permissions');
if (this._permissions.canI('connection:edit', 'Connection', this.connectionID)()) {
tabs.push('connection-settings', 'edit-db');
}
return tabs;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how CedarPermissionService.canI handles null resourceId
ast-grep --pattern $'canI($_, $_, $_) {
  $$$
}'
# Also check the service implementation
rg -n "canI\(" frontend/src/app/services/cedar-permission.service.ts -A 10

Repository: rocket-admin/rocketadmin

Length of output: 521


🏁 Script executed:

# Check the connectionID property definition at line 30
head -n 35 frontend/src/app/services/connections.service.ts | tail -n 10

Repository: rocket-admin/rocketadmin

Length of output: 306


🏁 Script executed:

# Check the full _evaluate method in CedarPermissionService
rg -n "_evaluate" frontend/src/app/services/cedar-permission.service.ts -A 20 | head -n 40

Repository: rocket-admin/rocketadmin

Length of output: 945


🏁 Script executed:

# Check if connectionID is typed as nullable
rg -n "connectionID" frontend/src/app/services/connections.service.ts -B 2 -A 2 | head -n 30

Repository: rocket-admin/rocketadmin

Length of output: 846


🏁 Script executed:

# Check the complete visibleTabs and surrounding context
sed -n '126,140p' frontend/src/app/services/connections.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 518


🏁 Script executed:

# Check if there are other places calling canI with connectionID to see if guards are used elsewhere
rg -n "canI.*connectionID" frontend/src/app/services/connections.service.ts -B 1 -A 1

Repository: rocket-admin/rocketadmin

Length of output: 394


🏁 Script executed:

# Check if cedar.isAuthorized is defensive about null resourceId
rg -n "id:" frontend/src/app/services/cedar-permission.service.ts -B 2 -A 2 | head -n 30

Repository: rocket-admin/rocketadmin

Length of output: 342


🏁 Script executed:

# Check if there are TypeScript type errors by looking at how visibleTabs and canEditConnection are called
rg -n "visibleTabs\|canEditConnection" frontend/src/app --type ts -B 2 -A 2 | head -n 50

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check if connectionID is ever guaranteed to be non-null at the point of calling visibleTabs
rg -n "connectionID.*=" frontend/src/app/services/connections.service.ts | head -n 20

Repository: rocket-admin/rocketadmin

Length of output: 144


🏁 Script executed:

# Check if there's any type assertion or suppression around these calls
rg -n "as string|// `@ts-ignore`|@ts-expect-error" frontend/src/app/services/connections.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check TypeScript compilation to see if there are type errors
rg -n "strictNullChecks\|noImplicitAny" tsconfig.json

Repository: rocket-admin/rocketadmin

Length of output: 121


🏁 Script executed:

# Check if there are other permission checks that guard against null values
rg -n "canI\(" frontend/src/app --type ts -B 2 -A 1 | grep -A 2 "if.*connectionID\|if.*\$\|if this\._" | head -n 30

Repository: rocket-admin/rocketadmin

Length of output: 876


🏁 Script executed:

# Check the actual TypeScript configuration
find frontend -name "tsconfig*" -type f

Repository: rocket-admin/rocketadmin

Length of output: 179


🏁 Script executed:

# Check strictNullChecks setting in tsconfig.json
cat frontend/tsconfig.json | grep -A 20 "compilerOptions"

Repository: rocket-admin/rocketadmin

Length of output: 573


🏁 Script executed:

# Check if the current code compiles without errors - look for strict null check issues
grep -n "strictNullChecks" frontend/tsconfig.app.json

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check if there are integration tests that exercise visibleTabs with null connectionID
rg -n "visibleTabs\|canEditConnection" frontend/src/app --type ts --type spec -A 3 -B 1 | head -n 50

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check what cedar.isAuthorized does with null resourceId at runtime - look for test cases
rg -n "resourceId.*null\|id:.*null" frontend/src/app/services/cedar-permission.service.spec.ts -B 2 -A 2

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check how often visibleTabs/canEditConnection are called and if they check connectionID first
rg -n "\.visibleTabs\|\.canEditConnection" frontend/src/app --type ts -B 5 | head -n 50

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check if cedar.isAuthorized has any test cases passing null or handling null gracefully
rg -n "cedar\.isAuthorized\|isAuthorized" frontend/src/app/services/cedar-permission.service.spec.ts -B 2 -A 5 | head -n 60

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Check if there are any existing patterns of guarding connectionID before using it
rg -n "if.*connectionID\|if.*this\.connectionID" frontend/src/app/services/connections.service.ts -B 1 -A 3

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Get the complete canI implementation to understand how it handles null at runtime
sed -n '43,75p' frontend/src/app/services/cedar-permission.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 1113


🏁 Script executed:

# Check the exact line numbers and code structure of visibleTabs in the actual file
sed -n '126,138p' frontend/src/app/services/connections.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 460


🏁 Script executed:

# Look for how canEditConnection is defined and if it should also have a guard
sed -n '126,128p' frontend/src/app/services/connections.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 180


🏁 Script executed:

# Check if there are other similar patterns in the codebase that DO guard against null
rg -n "if.*this\._" frontend/src/app/services/connections.service.ts | head -n 20

Repository: rocket-admin/rocketadmin

Length of output: 171


🏁 Script executed:

# Check if there are any test cases for visibleTabs or canEditConnection with null connectionID
rg -n "canEditConnection\|visibleTabs" frontend/src/app --type spec -B 3 -A 3

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Verify that passing null to cedar.isAuthorized could cause issues
sed -n '53,75p' frontend/src/app/services/cedar-permission.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 787


Add null guards before calling permission checks in visibleTabs and canEditConnection.

Both methods call canI() with this.connectionID, which can be null (line 30). Passing null to permission checks produces incorrect authorization results since the null value gets stringified in the cache key and passed to the Cedar authorization engine.

Add guards to both methods:

Fix for visibleTabs (lines 130-137)
get visibleTabs() {
  const tabs = ['dashboard', 'dashboards', 'audit'];
  if (this.connectionID) {
    if (this._permissions.canI('group:read', 'Group', this.connectionID)()) tabs.push('permissions');
    if (this.canEditConnection()) {
      tabs.push('connection-settings', 'edit-db');
    }
  }
  return tabs;
}

Also fix canEditConnection() (line 126-128) to guard against null:

canEditConnection() {
  return this.connectionID ? this._permissions.canI('connection:edit', 'Connection', this.connectionID)() : false;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/connections.service.ts` around lines 126 - 133,
visibleTabs and canEditConnection call this._permissions.canI(...) with
this.connectionID which may be null, causing incorrect auth cache keys; add null
guards so permission checks only run when this.connectionID is truthy: in
visibleTabs (method visibleTabs) wrap the permission branches (permissions tab
and connection-settings/edit-db tabs) inside an if (this.connectionID) block and
call canEditConnection() from there, and update canEditConnection() to return
false when this.connectionID is null (e.g., return this.connectionID ?
this._permissions.canI('connection:edit','Connection',this.connectionID)() :
false).

Comment on lines +90 to +95
async deleteGroup(groupId: string): Promise<void> {
await this._api.delete(`/group/${groupId}`, {
successMessage: 'Group has been removed.',
});
this._groupsUpdated.set('group-deleted');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how ApiService.delete handles errors and what it returns
ast-grep --pattern 'async delete($_) {
  $$$
}'

Repository: rocket-admin/rocketadmin

Length of output: 50


🏁 Script executed:

# Find ApiService definition
fd -t f "api.service.ts" frontend/

Repository: rocket-admin/rocketadmin

Length of output: 108


🏁 Script executed:

# Search for the ApiService class and its delete method
rg "class ApiService|export class ApiService" -A 50 frontend/

Repository: rocket-admin/rocketadmin

Length of output: 3927


🏁 Script executed:

# Look for the delete method implementation in ApiService
rg "delete\s*\(" -B 2 -A 10 frontend/src/app/services/ | head -100

Repository: rocket-admin/rocketadmin

Length of output: 2059


🏁 Script executed:

# Find the _fetchResponse method to understand error handling
rg "_fetchResponse" -A 30 frontend/src/app/services/api.service.ts | head -80

Repository: rocket-admin/rocketadmin

Length of output: 2507


🏁 Script executed:

# Also check if there's any try-catch in deleteGroup or similar mutation methods
cat -n frontend/src/app/services/users.service.ts | sed -n '77,139p'

Repository: rocket-admin/rocketadmin

Length of output: 2125


🏁 Script executed:

# Get the full _fetchResponse implementation to see complete error handling flow
rg "_fetchResponse" -A 50 frontend/src/app/services/api.service.ts | tail -100

Repository: rocket-admin/rocketadmin

Length of output: 2929


🏁 Script executed:

# Check how errors are actually handled - look for catch blocks
rg "catch|throw" -B 2 -A 3 frontend/src/app/services/api.service.ts | head -80

Repository: rocket-admin/rocketadmin

Length of output: 814


🏁 Script executed:

# Verify the return type of _api.delete() more explicitly
rg "async delete" frontend/src/app/services/api.service.ts -A 3

Repository: rocket-admin/rocketadmin

Length of output: 263


🏁 Script executed:

# Check if other mutation methods have the same issue
rg "this._groupsUpdated.set" -B 5 frontend/src/app/services/users.service.ts

Repository: rocket-admin/rocketadmin

Length of output: 1086


Event emitted unconditionally even when API call fails.

The deleteGroup method always emits the group-deleted event via this._groupsUpdated.set(), regardless of whether this._api.delete() succeeds or fails. Since _api.delete() returns null on error, the event should only be emitted on successful deletion to avoid out-of-sync state.

This pattern also affects editGroupName(), saveCedarPolicy(), addGroupUser(), and deleteGroupUser(). Compare with createGroup() which correctly checks the return value before emitting.

🛡️ Proposed fix
 async deleteGroup(groupId: string): Promise<void> {
-	await this._api.delete(`/group/${groupId}`, {
+	const result = await this._api.delete(`/group/${groupId}`, {
 		successMessage: 'Group has been removed.',
 	});
-	this._groupsUpdated.set('group-deleted');
+	if (result !== null) {
+		this._groupsUpdated.set('group-deleted');
+	}
 }

Apply the same fix to the other affected mutation methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 90 - 95, The methods
deleteGroup, editGroupName, saveCedarPolicy, addGroupUser, and deleteGroupUser
currently call this._groupsUpdated.set('group-...') (or similar)
unconditionally; change each to mirror createGroup by checking the API call's
return value (the _api.* call returns null on error) and only emit the update
event when the call succeeded (non-null/truthy response), i.e., perform the
this._groupsUpdated.set(...) inside an if that verifies the API result was
successful.

Comment on lines +97 to +139
async editGroupName(groupId: string, title: string): Promise<void> {
await this._api.put(
'/group/title',
{ title, groupId },
{
successMessage: 'Group name has been updated.',
},
);
this._groupsUpdated.set('group-renamed');
}

fetcGroupUsers(groupID: string) {
return this._http.get<any>(`/group/users/${groupID}`).pipe(
map((res) => res),
catchError((err) => {
console.log(err);
this._notifications.showErrorSnackbar(err.error?.message || err.message);
return EMPTY;
}),
async saveCedarPolicy(connectionId: string, groupId: string, cedarPolicy: string): Promise<void> {
await this._api.post(
`/connection/cedar-policy/${connectionId}`,
{ cedarPolicy, groupId },
{
successMessage: 'Policy has been saved.',
},
);
this._groupsUpdated.set('policy-saved');
}

createUsersGroup(connectionID: string, title: string) {
return this._http.post<any>(`/connection/group/${connectionID}`, { title }).pipe(
map((res) => {
this.groups.next({ action: 'add group', group: res });
this._notifications.showSuccessSnackbar('Group of users has been created.');
return res;
}),
catchError((err) => {
console.log(err);
this._notifications.showErrorSnackbar(err.error?.message || err.message);
return EMPTY;
}),
async addGroupUser(groupId: string, email: string): Promise<void> {
await this._api.put(
'/group/user',
{ email, groupId },
{
successMessage: 'User has been added to group.',
},
);
this._groupsUpdated.set('user-added');
}

saveCedarPolicy(connectionID: string, groupId: string, cedarPolicy: string) {
return this._http.post<any>(`/connection/cedar-policy/${connectionID}`, { cedarPolicy, groupId }).pipe(
map((res) => {
this.groups.next({ action: 'save policy', groupId });
this._notifications.showSuccessSnackbar('Policy has been saved.');
return res;
}),
async deleteGroupUser(email: string, groupId: string): Promise<void> {
await this._api.put(
'/group/user/delete',
{ email, groupId },
{
successMessage: 'User has been removed from group.',
},
);
this._groupsUpdated.set('user-deleted');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue: mutation methods emit events without verifying success.

editGroupName, saveCedarPolicy, addGroupUser, and deleteGroupUser all emit groupsUpdated events unconditionally. If the underlying API calls fail (throw or return null), these events could trigger UI refreshes for operations that didn't actually complete.

Consider applying the same pattern as createGroup (line 86) which checks if (group) before emitting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 97 - 139, The four
mutation methods editGroupName, saveCedarPolicy, addGroupUser, and
deleteGroupUser currently call this._groupsUpdated.set(...) unconditionally;
change each to capture the API call result (e.g., const res = await
this._api.put/post(...)) and only call this._groupsUpdated.set('...') when the
response is truthy/successful (same pattern as createGroup), so the event is
emitted only on successful operations.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts (1)

203-205: Consider relocating getter to maintain component structure.

The editActionGroups getter is placed between public methods (startEdit and saveEdit). As per component structure guidelines, derived state (getters/computed) should be placed before public methods.

Consider moving this getter up near lines 71-73 where other derived state is declared.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 203 - 205, The editActionGroups getter (which calls
_buildFilteredGroups(this.editingIndex ?? -1)) is currently located between
public methods startEdit and saveEdit; move this derived-state getter up into
the component's derived/computed section (near where other getters around lines
with existing derived state are declared, e.g. alongside other getters at
~71-73) so that all computed/derived properties appear before public methods;
ensure references to editingIndex and _buildFilteredGroups remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/app/components/users/users.component.html`:
- Around line 98-103: The self-remove guard allows a transient truthy comparison
when currentUser() is unset; tighten the condition by explicitly requiring
currentUser() to be present before comparing emails. Replace the current
conditional (currentUser()?.email !== user.email && canManage()) with an
explicit presence check such as (currentUser() && currentUser().email !==
user.email && canManage()) so the Delete button (which calls
openDeleteUserDialog) is only shown after currentUser() is defined and its email
is confirmed different from user.email.

---

Nitpick comments:
In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 203-205: The editActionGroups getter (which calls
_buildFilteredGroups(this.editingIndex ?? -1)) is currently located between
public methods startEdit and saveEdit; move this derived-state getter up into
the component's derived/computed section (near where other getters around lines
with existing derived state are declared, e.g. alongside other getters at
~71-73) so that all computed/derived properties appear before public methods;
ensure references to editingIndex and _buildFilteredGroups remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06ac6085-e198-4469-b0bc-7250d269edbd

📥 Commits

Reviewing files that changed from the base of the PR and between 7948498 and 54b9a6e.

📒 Files selected for processing (13)
  • frontend/src/app/components/connect-db/connect-db.component.spec.ts
  • frontend/src/app/components/connect-db/connect-db.component.ts
  • frontend/src/app/components/connection-settings/connection-settings.component.ts
  • frontend/src/app/components/dashboard/dashboard.component.spec.ts
  • frontend/src/app/components/dashboard/dashboard.component.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts
  • frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts
  • frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts
  • frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.ts
  • frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts
  • frontend/src/app/components/users/users.component.html
  • frontend/src/app/services/connections.service.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/app/components/connect-db/connect-db.component.ts
  • frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts
  • frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts

Comment on lines +98 to +103
@if (currentUser()?.email !== user.email && canManage()) {
<button type="button" mat-icon-button
angulartics2On="click"
angularticsAction="Users access: delete user is clicked"
matTooltip="Delete user"
(click)="openDeleteUserDialog(user, groupItem.group); posthog.capture('Users access: delete user is clicked')">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten the self-remove guard.

When currentUser() is still unset, currentUser()?.email !== user.email evaluates to true, so the signed-in user can briefly see their own remove button until that signal resolves.

Suggested fix
+                                        `@let` currentUserEmail = currentUser()?.email;
-                                        `@if` (currentUser()?.email !== user.email && canManage()) {
+                                        `@if` (currentUserEmail && currentUserEmail !== user.email && canManage()) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@if (currentUser()?.email !== user.email && canManage()) {
<button type="button" mat-icon-button
angulartics2On="click"
angularticsAction="Users access: delete user is clicked"
matTooltip="Delete user"
(click)="openDeleteUserDialog(user, groupItem.group); posthog.capture('Users access: delete user is clicked')">
`@let` currentUserEmail = currentUser()?.email;
`@if` (currentUserEmail && currentUserEmail !== user.email && canManage()) {
<button type="button" mat-icon-button
angulartics2On="click"
angularticsAction="Users access: delete user is clicked"
matTooltip="Delete user"
(click)="openDeleteUserDialog(user, groupItem.group); posthog.capture('Users access: delete user is clicked')">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.html` around lines 98 -
103, The self-remove guard allows a transient truthy comparison when
currentUser() is unset; tighten the condition by explicitly requiring
currentUser() to be present before comparing emails. Replace the current
conditional (currentUser()?.email !== user.email && canManage()) with an
explicit presence check such as (currentUser() && currentUser().email !==
user.email && canManage()) so the Delete button (which calls
openDeleteUserDialog) is only shown after currentUser() is defined and its email
is confirmed different from user.email.

… update

Previously, saveCedarPolicy only set a groupsUpdated signal that was consumed
by an effect in UsersComponent. If that component wasn't mounted, the groups
resource never reloaded and CedarPermissionService kept evaluating stale policies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
frontend/src/app/components/users/users.component.ts (2)

105-107: ⚠️ Potential issue | 🟠 Major

Make connection-scoped state follow the current connection instead of one-time reads.

This component stores currentConnectionID locally, but never seeds UsersService.setActiveConnection() from it, and canCreateGroup captures _connections.currentConnectionID once at field initialization. That leaves group loading and create-permission checks dependent on whichever connection id happened to exist before ngOnInit().

Also applies to: 126-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.ts` around lines 105 - 107,
The component currently snapshots _connections.currentConnectionID once (at
field init and in ngOnInit) which breaks connection-scoped state; change it to
react to connection changes by calling UsersService.setActiveConnection whenever
the connection ID changes and by computing canCreateGroup from the live
connection (e.g., subscribe to _connections.connectionId$ or
_connections.onChange and inside that subscription set this.connectionID and
call this.usersService.setActiveConnection(connectionID)), remove any one-time
capture of _connections.currentConnectionID (including the canCreateGroup field
initialization) and replace with either a getter or observable-driven value so
group loading and permission checks always use the current connection ID.

117-123: ⚠️ Potential issue | 🟡 Minor

The inner company-members subscription is still unbounded.

The outer cast subscription is torn down, but each inner fetchCompanyMembers() subscription lives independently. If cast emits again before an earlier request completes, those requests can overlap and race to update companyMembers.

🛠️ Suggested change
-import { take } from 'rxjs';
+import { switchMap, take, tap } from 'rxjs';
@@
-		this._userService.cast.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((user) => {
-			this.currentUser.set(user);
-
-			this._company.fetchCompanyMembers(user.company.id).subscribe((members) => {
-				this.companyMembers.set(members);
-			});
-		});
+		this._userService.cast
+			.pipe(
+				takeUntilDestroyed(this._destroyRef),
+				tap((user) => {
+					this.currentUser.set(user);
+				}),
+				switchMap((user) => this._company.fetchCompanyMembers(user.company.id)),
+			)
+			.subscribe((members) => {
+				this.companyMembers.set(members);
+			});

Based on learnings: Applies to frontend/**/*.component.ts : Use takeUntil pattern for memory leak prevention with proper subscription management.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.ts` around lines 117 - 123,
The inner subscription to _company.fetchCompanyMembers is unbounded and can
overlap; refactor the _userService.cast pipeline to avoid nested subscribes by
using an operator like switchMap: pipe takeUntilDestroyed(this._destroyRef), tap
to call currentUser.set(user), then switchMap(user =>
_company.fetchCompanyMembers(user.company.id)) so the previous request is
cancelled if a new user emits, and finally subscribe once to set companyMembers
(companyMembers.set). Ensure the takeUntilDestroyed(this._destroyRef) remains on
the outer stream so teardown works.
frontend/src/app/services/users.service.ts (1)

90-106: ⚠️ Potential issue | 🟠 Major

Only emit groupsUpdated after a successful mutation.

deleteGroup, editGroupName, addGroupUser, and deleteGroupUser still fire update events unconditionally. createGroup() already shows the safer pattern in this file: gate the event behind a successful response so failed writes do not trigger stale refreshes or success-looking UI flows.

Also applies to: 120-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 90 - 106, The
deleteGroup, editGroupName, addGroupUser, and deleteGroupUser functions
currently call this._groupsUpdated unconditionally; change them to only emit
after confirming the API call succeeded (matching the safer pattern used in
createGroup). Specifically, capture the result of this._api.delete /
this._api.put / this._api.post calls, check the success/ok flag returned by the
API response (or that no error was thrown), and only call
this._groupsUpdated.set('...') when the response indicates a successful
mutation; update deleteGroup, editGroupName, addGroupUser, and deleteGroupUser
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/app/services/users.service.ts`:
- Around line 108-117: The saveCedarPolicy function is refreshing groups using a
generic refreshGroups() that may use a stale _activeConnectionId; change
saveCedarPolicy to explicitly reload the connection just updated by calling the
refresh method with the provided connectionId (e.g., call
refreshGroups(connectionId) or pass connectionId into whatever helper
refreshGroups accepts) so the saved policy’s groups are reloaded for the correct
connection; keep the _groupsUpdated.set('policy-saved') line as-is after the
explicit refresh.
- Around line 63-70: The fetchGroupUsers function currently masks request
failures by using `users ?? []` and always writing `'empty'` to the
`_groupUsers` cache; change it so that you only write `'empty'` when the API
actually returned an empty array, and do not overwrite the existing
`_groupUsers` entry on request failure. Concretely, in `fetchGroupUsers(groupId:
string)` stop coercing `users` to `[]`, check whether the API returned
`null`/`undefined` or threw an error; if `users` is an actual empty array set
`_groupUsers.update(... [groupId]: 'empty')`, if it is a non-empty array set the
array, and if the call failed (or returned undefined/null) leave the cache
untouched (or rethrow/log) so previous entries remain intact. Ensure you
reference `_api.get<GroupUser[]>`, `_groupUsers.update`, and `fetchGroupUsers`
when making the change.

---

Duplicate comments:
In `@frontend/src/app/components/users/users.component.ts`:
- Around line 105-107: The component currently snapshots
_connections.currentConnectionID once (at field init and in ngOnInit) which
breaks connection-scoped state; change it to react to connection changes by
calling UsersService.setActiveConnection whenever the connection ID changes and
by computing canCreateGroup from the live connection (e.g., subscribe to
_connections.connectionId$ or _connections.onChange and inside that subscription
set this.connectionID and call
this.usersService.setActiveConnection(connectionID)), remove any one-time
capture of _connections.currentConnectionID (including the canCreateGroup field
initialization) and replace with either a getter or observable-driven value so
group loading and permission checks always use the current connection ID.
- Around line 117-123: The inner subscription to _company.fetchCompanyMembers is
unbounded and can overlap; refactor the _userService.cast pipeline to avoid
nested subscribes by using an operator like switchMap: pipe
takeUntilDestroyed(this._destroyRef), tap to call currentUser.set(user), then
switchMap(user => _company.fetchCompanyMembers(user.company.id)) so the previous
request is cancelled if a new user emits, and finally subscribe once to set
companyMembers (companyMembers.set). Ensure the
takeUntilDestroyed(this._destroyRef) remains on the outer stream so teardown
works.

In `@frontend/src/app/services/users.service.ts`:
- Around line 90-106: The deleteGroup, editGroupName, addGroupUser, and
deleteGroupUser functions currently call this._groupsUpdated unconditionally;
change them to only emit after confirming the API call succeeded (matching the
safer pattern used in createGroup). Specifically, capture the result of
this._api.delete / this._api.put / this._api.post calls, check the success/ok
flag returned by the API response (or that no error was thrown), and only call
this._groupsUpdated.set('...') when the response indicates a successful
mutation; update deleteGroup, editGroupName, addGroupUser, and deleteGroupUser
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44469e08-e95e-47d8-ae40-8b4a7fabab04

📥 Commits

Reviewing files that changed from the base of the PR and between 54b9a6e and 9069b24.

📒 Files selected for processing (2)
  • frontend/src/app/components/users/users.component.ts
  • frontend/src/app/services/users.service.ts

Comment on lines +69 to +74
protected companyMembersWithoutAccess = computed(() => {
const members = this.companyMembers();
const users = this.groupUsers();
const allGroupUsers = Object.values(users).flat();
return differenceBy(members, allGroupUsers, 'email');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Limit companyMembersWithoutAccess to the groups on this page.

groupUsers() is a root-scoped cache, so Object.values(users).flat() also pulls in members from deleted groups or a previous connection. That makes differenceBy exclude people who are not actually assigned to any current group.

🛠️ Suggested change
 protected companyMembersWithoutAccess = computed(() => {
 	const members = this.companyMembers();
-	const users = this.groupUsers();
-	const allGroupUsers = Object.values(users).flat();
+	const groups = this.groups() ?? [];
+	const usersByGroup = this.groupUsers();
+	const allGroupUsers = groups.flatMap(({ group }) => {
+		const users = usersByGroup[group.id];
+		return Array.isArray(users) ? users : [];
+	});
 	return differenceBy(members, allGroupUsers, 'email');
 });

Comment on lines +63 to +70
async fetchGroupUsers(groupId: string): Promise<GroupUser[]> {
const users = await this._api.get<GroupUser[]>(`/group/users/${groupId}`);
const result = users ?? [];
this._groupUsers.update((current) => ({
...current,
[groupId]: result.length ? result : 'empty',
}));
return result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't turn a failed group-user fetch into an empty group.

users ?? [] makes request failures indistinguishable from a real empty result, so the cache gets overwritten with 'empty'. After a transient error, the UI will treat the group as having no members and can offer duplicate adds. Only store 'empty' when the backend actually returned an empty array; keep the previous cache entry on failure.

🛠️ Suggested change
 async fetchGroupUsers(groupId: string): Promise<GroupUser[]> {
 	const users = await this._api.get<GroupUser[]>(`/group/users/${groupId}`);
-	const result = users ?? [];
+	if (users == null) {
+		const cached = this._groupUsers()[groupId];
+		return Array.isArray(cached) ? cached : [];
+	}
 	this._groupUsers.update((current) => ({
 		...current,
-		[groupId]: result.length ? result : 'empty',
+		[groupId]: users.length ? users : 'empty',
 	}));
-	return result;
+	return users;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 63 - 70, The
fetchGroupUsers function currently masks request failures by using `users ?? []`
and always writing `'empty'` to the `_groupUsers` cache; change it so that you
only write `'empty'` when the API actually returned an empty array, and do not
overwrite the existing `_groupUsers` entry on request failure. Concretely, in
`fetchGroupUsers(groupId: string)` stop coercing `users` to `[]`, check whether
the API returned `null`/`undefined` or threw an error; if `users` is an actual
empty array set `_groupUsers.update(... [groupId]: 'empty')`, if it is a
non-empty array set the array, and if the call failed (or returned
undefined/null) leave the cache untouched (or rethrow/log) so previous entries
remain intact. Ensure you reference `_api.get<GroupUser[]>`,
`_groupUsers.update`, and `fetchGroupUsers` when making the change.

Comment on lines +108 to +117
async saveCedarPolicy(connectionId: string, groupId: string, cedarPolicy: string): Promise<void> {
await this._api.post(
`/connection/cedar-policy/${connectionId}`,
{ cedarPolicy, groupId },
{
successMessage: 'Policy has been saved.',
},
);
this.refreshGroups();
this._groupsUpdated.set('policy-saved');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reload the connection that was just updated.

saveCedarPolicy() already receives connectionId, but the refresh path ignores it and reloads whatever _activeConnectionId currently points to. If that signal is stale or unset, the policy save succeeds without refreshing the edited connection’s groups, which undercuts the new “refresh directly after save” path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/services/users.service.ts` around lines 108 - 117, The
saveCedarPolicy function is refreshing groups using a generic refreshGroups()
that may use a stale _activeConnectionId; change saveCedarPolicy to explicitly
reload the connection just updated by calling the refresh method with the
provided connectionId (e.g., call refreshGroups(connectionId) or pass
connectionId into whatever helper refreshGroups accepts) so the saved policy’s
groups are reloaded for the correct connection; keep the
_groupsUpdated.set('policy-saved') line as-is after the explicit refresh.

gugu and others added 2 commits March 26, 2026 15:59
dashboard:create targets the connection, not an existing dashboard.
Set needsDashboard to false, generate a Connection resource in the
cedar policy, and parse it without expecting a dashboard resource suffix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html (1)

31-35: ⚠️ Potential issue | 🔴 Critical

Add button does not submit the form.

At Line 31, type="button" prevents (ngSubmit)="joinGroupUser()" from firing, and there is no explicit click handler. Users clicking Add will get no action.

🐛 Proposed fix
-      <button type="button"
+      <button type="submit"
         mat-flat-button color="primary"
         [disabled]="submitting() || addUserForm.form.invalid">
         Add
       </button>
#!/bin/bash
# Verify current template wiring for submit behavior.
rg -nP '\(ngSubmit\)|type="button"|type="submit"|\(click\)|>\\s*Add\\s*<' frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html`
around lines 31 - 35, The Add button uses type="button" so the form's
(ngSubmit)="joinGroupUser()" never fires; change the button to submit the form
by replacing type="button" with type="submit" (or alternatively add an explicit
(click)="joinGroupUser()" handler) so clicking the button triggers the
joinGroupUser() flow and respects addUserForm.form.invalid and submitting()
checks; target the Add button element in the template where addUserForm and
joinGroupUser() are used.
frontend/src/app/components/users/users.component.html (2)

98-106: ⚠️ Potential issue | 🟡 Minor

Tighten the self-remove guard to handle unresolved currentUser().

When currentUser() is still null/undefined, currentUser()?.email returns undefined, and undefined !== user.email evaluates to true. This briefly exposes the delete button for the signed-in user until the signal resolves.

🛡️ Proposed fix
-                                       `@if` (currentUser()?.email !== user.email && canManage()) {
+                                       `@if` (currentUser() && currentUser()!.email !== user.email && canManage()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.html` around lines 98 -
106, The delete-button guard should require currentUser() to be resolved before
comparing emails: replace the loose check that uses currentUser()?.email with a
truthy check that currentUser() exists and then compare its email against
user.email, keeping the existing canManage() check and the (click) handler that
calls openDeleteUserDialog and posthog.capture; this ensures the button is not
shown while currentUser() is null/undefined.

31-40: ⚠️ Potential issue | 🟡 Minor

Add $event.stopPropagation() to prevent panel toggle on edit click.

The edit button sits inside mat-expansion-panel-header without stopping propagation. Clicking it will toggle the panel while also opening the dialog.

🛠️ Proposed fix
-                                (click)="openEditGroupNameDialog($event, groupItem.group); posthog.capture('Users access: edit group name is clicked')">
+                                (click)="$event.stopPropagation(); openEditGroupNameDialog($event, groupItem.group); posthog.capture('Users access: edit group name is clicked')">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/users/users.component.html` around lines 31 - 40,
The edit button click handler inside the mat-expansion-panel-header should stop
event propagation to prevent the panel from toggling; update the click handler
on the button that calls openEditGroupNameDialog($event, groupItem.group) and
posthog.capture(...) so it first invokes $event.stopPropagation() (e.g., call
$event.stopPropagation() before calling openEditGroupNameDialog and
posthog.capture) to ensure the panel doesn't expand/collapse when the edit
dialog opens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html`:
- Around line 31-35: The Add button uses type="button" so the form's
(ngSubmit)="joinGroupUser()" never fires; change the button to submit the form
by replacing type="button" with type="submit" (or alternatively add an explicit
(click)="joinGroupUser()" handler) so clicking the button triggers the
joinGroupUser() flow and respects addUserForm.form.invalid and submitting()
checks; target the Add button element in the template where addUserForm and
joinGroupUser() are used.

In `@frontend/src/app/components/users/users.component.html`:
- Around line 98-106: The delete-button guard should require currentUser() to be
resolved before comparing emails: replace the loose check that uses
currentUser()?.email with a truthy check that currentUser() exists and then
compare its email against user.email, keeping the existing canManage() check and
the (click) handler that calls openDeleteUserDialog and posthog.capture; this
ensures the button is not shown while currentUser() is null/undefined.
- Around line 31-40: The edit button click handler inside the
mat-expansion-panel-header should stop event propagation to prevent the panel
from toggling; update the click handler on the button that calls
openEditGroupNameDialog($event, groupItem.group) and posthog.capture(...) so it
first invokes $event.stopPropagation() (e.g., call $event.stopPropagation()
before calling openEditGroupNameDialog and posthog.capture) to ensure the panel
doesn't expand/collapse when the edit dialog opens.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da84a439-02aa-418d-b2fb-45c9bdcde996

📥 Commits

Reviewing files that changed from the base of the PR and between d012a4c and 9993894.

📒 Files selected for processing (2)
  • frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.html
  • frontend/src/app/components/users/users.component.html

gugu and others added 2 commits March 27, 2026 12:29
The "Add" button in user-add-dialog had type="button" with no click
handler, so clicking it did nothing. Changed to type="submit" to
trigger the form's ngSubmit. Also rewrote tests with comprehensive
coverage for both available-members and empty-members states.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ring checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants